#! /usr/bin/env python # -*- coding: utf-8 -*- # eLyXer -- convert LyX source files to HTML output. # # Copyright (C) 2009 Alex Fernández # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # --end-- # Alex 20090308 # eLyXer main script # http://www.nongnu.org/elyxer/ import sys import os.path import os.path import sys import codecs import sys class Trace(object): "A tracing class" debugmode = False quietmode = False showlinesmode = False prefix = None def debug(cls, message): "Show a debug message" if not Trace.debugmode or Trace.quietmode: return Trace.show(message, sys.stdout) def message(cls, message): "Show a trace message" if Trace.quietmode: return if Trace.prefix and Trace.showlinesmode: message = Trace.prefix + message Trace.show(message, sys.stdout) def error(cls, message): "Show an error message" if Trace.prefix and Trace.showlinesmode: message = Trace.prefix + message Trace.show(message, sys.stderr) def fatal(cls, message): "Show an error message and terminate" Trace.error('FATAL: ' + message) exit(-1) def show(cls, message, channel): "Show a message out of a channel" message = message.encode('utf-8') channel.write(message + '\n') debug = classmethod(debug) message = classmethod(message) error = classmethod(error) fatal = classmethod(fatal) show = classmethod(show) class LineReader(object): "Reads a file line by line" def __init__(self, filename): if isinstance(filename, file): self.file = filename else: self.file = codecs.open(filename, 'rU', "utf-8") self.linenumber = 1 self.lastline = None self.current = None self.mustread = True self.depleted = False def setstart(self, firstline): "Set the first line to read." for i in range(firstline): self.file.readline() self.linenumber = firstline def setend(self, lastline): "Set the last line to read." self.lastline = lastline def currentline(self): "Get the current line" if self.mustread: self.readline() return self.current def nextline(self): "Go to next line" if self.depleted: Trace.fatal('Read beyond file end') self.mustread = True def readline(self): "Read a line from file" self.current = self.file.readline() if self.file == sys.stdin: self.current = self.current.decode('utf-8') if len(self.current) == 0: self.depleted = True self.current = self.current.rstrip('\n\r') self.linenumber += 1 self.mustread = False Trace.prefix = 'Line ' + unicode(self.linenumber) + ': ' if self.linenumber % 1000 == 0: Trace.message('Parsing') def finished(self): "Find out if the file is finished" if self.lastline and self.linenumber == self.lastline: return True if self.mustread: self.readline() return self.depleted def close(self): self.file.close() class LineWriter(object): "Writes a file as a series of lists" def __init__(self, filename): if isinstance(filename, file): self.file = filename self.filename = None else: self.file = codecs.open(filename, 'w', "utf-8") self.filename = filename def write(self, strings): "Write a list of strings" for string in strings: if not isinstance(string, basestring): Trace.error('Not a string: ' + unicode(string) + ' in ' + unicode(strings)) return self.writestring(string) def writestring(self, string): "Write a string" if self.file == sys.stdout: string = string.encode('utf-8') self.file.write(string) def writeline(self, line): "Write a line to file" self.writestring(line + '\n') def close(self): self.file.close() import codecs import os.path import codecs class BibStylesConfig(object): "Configuration class from config file" authordate2 = { u'@article':u'$author. $year. $title. $journal, $volume($number), $pages.', u'@book':u'$author. $year. $title. $publisher.', u'default':u'$author. $year. $title. $publisher.', } default = { u'@article':u'$author, “$title”, $journal, pp. $pages, $year.', u'@book':u'$author, $title. $publisher, $year.', u'@booklet':u'$author, $title. $publisher, $year.', u'@conference':u'$author, “$title”, $journal, pp. $pages, $year.', u'@inbook':u'$author, $title. $publisher, $year.', u'@incollection':u'$author, $title. $publisher, $year.', u'@inproceedings':u'$author, “$title”, $journal, pp. $pages, $year.', u'@manual':u'$author, $title. $publisher, $year.', u'@mastersthesis':u'$author, $title. $publisher, $year.', u'@misc':u'$author, $title. $publisher, $year.', u'@phdthesis':u'$author, $title. $publisher, $year.', u'@proceedings':u'$author, “$title”, $journal, pp. $pages, $year.', u'@techreport':u'$author, $title, $year.', u'@unpublished':u'$author, “$title”, $journal, $year.', u'default':u'$author, $title. $publisher, $year.', } ieeetr = { u'@article':u'$author, “$title”, $journal, vol. $volume, no. $number, pp. $pages, $year.', u'@book':u'$author, $title. $publisher, $year.', } plain = { u'@article':u'$author. $title. $journal, $volumen($number):$pages, $year.', u'@book':u'$author. $title. $publisher, $month $year.', u'default':u'$author. $title. $publisher, $year.', } class ContainerConfig(object): "Configuration class from config file" endings = { u'Align':u'\\end_layout', u'BarredText':u'\\bar', u'BoldText':u'\\series', u'Cell':u'':u'>', } html = { u'/>':u'>', } nonunicode = { u' ':u' ', } class FileConfig(object): "Configuration class from config file" parsing = { u'encodings':[u'utf-8',u'Cp1252',], } class FootnoteConfig(object): "Configuration class from config file" constants = { u'postfrom':u'] ', u'postto':u'→] ', u'prefrom':u'[→', u'preto':u' [', } class FormulaConfig(object): "Configuration class from config file" alphacommands = { u'\\AA':u'Å', u'\\AE':u'Æ', u'\\Delta':u'Δ', u'\\Gamma':u'Γ', u'\\L':u'Ł', u'\\Lambda':u'Λ', u'\\O':u'Ø', u'\\OE':u'Œ', u'\\Omega':u'Ω', u'\\Phi':u'Φ', u'\\Pi':u'Π', u'\\Psi':u'Ψ', u'\\Sigma':u'Σ', u'\\Theta':u'Θ', u'\\Upsilon':u'Υ', u'\\Xi':u'Ξ', u'\\aa':u'å', u'\\ae':u'æ', u'\\alpha':u'α', u'\\beta':u'β', u'\\delta':u'δ', u'\\epsilon':u'ϵ', u'\\eta':u'η', u'\\gamma':u'γ', u'\\iota':u'ι', u'\\kappa':u'κ', u'\\l':u'ł', u'\\lambda':u'λ', u'\\mu':u'μ', u'\\nu':u'ν', u'\\o':u'ø', u'\\oe':u'œ', u'\\omega':u'ω', u'\\phi':u'φ', u'\\pi':u'π', u'\\psi':u'ψ', u'\\rho':u'ρ', u'\\sigma':u'σ', u'\\ss':u'ß', u'\\tau':u'τ', u'\\theta':u'θ', u'\\upsilon':u'υ', u'\\varepsilon':u'ε', u'\\varkappa':u'ϰ', u'\\varphi':u'φ', u'\\varpi':u'ϖ', u'\\varrho':u'ϱ', u'\\varsigma':u'ς', u'\\vartheta':u'ϑ', u'\\xi':u'ξ', u'\\zeta':u'ζ', } array = { u'begin':u'\\begin', u'cellseparator':u'&', u'end':u'\\end', u'rowseparator':u'\\\\', } commands = { u'\\!':u'', u'\\$':u'$', u'\\%':u'%', u'\\,':u' ', u'\\:':u' ', u'\\;':u' ', u'\\Box':u'□', u'\\CIRCLE':u'●', u'\\CheckedBox':u'☑', u'\\Circle':u'○', u'\\Diamond':u'◇', u'\\Downarrow':u'⇓', u'\\Im':u'ℑ', u'\\Join':u'⨝', u'\\LEFTCIRCLE':u'◖', u'\\LEFTcircle':u'◐', u'\\Leftarrow':u'⇐', u'\\Leftrightarrow':u' ⇔ ', u'\\Longleftarrow':u'⟸', u'\\Longleftrightarrow':u'⟺', u'\\Longrightarrow':u'⟹', u'\\P':u'¶', u'\\Pr':u'Pr', u'\\RIGHTCIRCLE':u'◗', u'\\RIGHTcircle':u'◑', u'\\Re':u'ℜ', u'\\Rightarrow':u' ⇒ ', u'\\S':u'§', u'\\Square':u'☐', u'\\Uparrow':u'⇑', u'\\Updownarrow':u'⇕', u'\\Vert':u'∥', u'\\XBox':u'☒', u'\\\\':u'
', u'\\_':u'_', u'\\aleph':u'ℵ', u'\\amalg':u'∐', u'\\angle':u'∠', u'\\approx':u' ≈ ', u'\\aquarius':u'♒', u'\\arccos':u'arccos', u'\\arcsin':u'arcsin', u'\\arctan':u'arctan', u'\\arg':u'arg', u'\\aries':u'♈', u'\\ast':u'∗', u'\\asymp':u'≍', u'\\backprime':u'‵', u'\\backslash':u'\\', u'\\beth':u'ℶ', u'\\bigcap':u'∩', u'\\bigcirc':u'○', u'\\bigcup':u'∪', u'\\bigodot':u'⊙', u'\\bigoplus':u'⊕', u'\\bigotimes':u'⊗', u'\\bigsqcup':u'⊔', u'\\bigstar':u'★', u'\\bigtriangledown':u'▽', u'\\bigtriangleup':u'△', u'\\biguplus':u'⊎', u'\\bigvee':u'∨', u'\\bigwedge':u'∧', u'\\blacklozenge':u'⧫', u'\\blacksmiley':u'☻', u'\\blacktriangle':u'▲', u'\\blacktriangledown':u'▼', u'\\blacktriangleright':u'▶', u'\\bot':u'⊥', u'\\bowtie':u'⋈', u'\\box':u'▫', u'\\bullet':u'•', u'\\cancer':u'♋', u'\\cap':u'∩', u'\\capricornus':u'♑', u'\\cdot':u'⋅', u'\\cdots':u'⋯', u'\\centerdot':u'∙', u'\\checkmark':u'✓', u'\\chi':u'χ', u'\\circ':u'○', u'\\circeq':u'≗', u'\\circledR':u'®', u'\\clubsuit':u'♣', u'\\complement':u'∁', u'\\cong':u'≅', u'\\coprod':u'∐', u'\\copyright':u'©', u'\\cos':u'cos', u'\\cosh':u'cosh', u'\\cot':u'cot', u'\\coth':u'coth', u'\\csc':u'csc', u'\\cup':u'∪', u'\\dag':u'†', u'\\dagger':u'†', u'\\daleth':u'ℸ', u'\\dashrightarrow':u' ⇢ ', u'\\dashv':u'⊣', u'\\ddag':u'‡', u'\\ddagger':u'‡', u'\\ddots':u'⋱', u'\\deg':u'deg', u'\\det':u'det', u'\\diamond':u'◇', u'\\diamondsuit':u'♦', u'\\dim':u'dim', u'\\displaystyle':u'', u'\\div':u'÷', u'\\doteq':u'≐', u'\\dots':u'…', u'\\downarrow':u'↓', u'\\earth':u'♁', u'\\ell':u'ℓ', u'\\emptyset':u'∅', u'\\equiv':u' ≡ ', u'\\euro':u'€', u'\\exists':u'∃', u'\\exp':u'exp', u'\\female':u'♀', u'\\flat':u'♭', u'\\forall':u'∀', u'\\frown':u'⌢', u'\\frownie':u'☹', u'\\gcd':u'gcd', u'\\ge':u' ≥ ', u'\\gemini':u'♊', u'\\geq':u' ≥ ', u'\\geq)':u'≥', u'\\gets':u'←', u'\\gg':u'≫', u'\\gimel':u'ℷ', u'\\gtrless':u'≷', u'\\hbar':u'ℏ', u'\\heartsuit':u'♥', u'\\hfill':u' ', u'\\hom':u'hom', u'\\hookleftarrow':u'↩', u'\\hookrightarrow':u'↪', u'\\hslash':u'ℏ', u'\\imath':u'ı', u'\\implies':u'  ⇒  ', u'\\in':u' ∈ ', u'\\inf':u'inf', u'\\infty':u'∞', u'\\int':u'', u'\\intop':u'', u'\\invneg':u'⌐', u'\\jmath':u'ȷ', u'\\jupiter':u'♃', u'\\ker':u'ker', u'\\land':u'∧', u'\\langle':u'⟨', u'\\lbrace':u'{', u'\\lbrace)':u'{', u'\\lbrack':u'[', u'\\lceil':u'⌈', u'\\ldots':u'…', u'\\le':u'≤', u'\\leadsto':u'⇝', u'\\leftarrow':u' ← ', u'\\leftarrow)':u'←', u'\\leftharpoondown':u'↽', u'\\leftharpoonup':u'↼', u'\\leftmoon':u'☾', u'\\leftrightarrow':u'↔', u'\\leo':u'♌', u'\\leq':u' ≤ ', u'\\leq)':u'≤', u'\\lessgtr':u'≶', u'\\lfloor':u'⌊', u'\\lg':u'lg', u'\\lhd':u'⊲', u'\\libra':u'♎', u'\\lim':u'lim', u'\\liminf':u'liminf', u'\\limsup':u'limsup', u'\\ll':u'≪', u'\\ln':u'ln', u'\\lnot':u'¬', u'\\log':u'log', u'\\longleftarrow':u'⟵', u'\\longleftrightarrow':u'⟷', u'\\longmapsto':u'⟼', u'\\longrightarrow':u'⟶', u'\\lor':u'∨', u'\\lozenge':u'◊', u'\\lyxlock':u'', u'\\male':u'♂', u'\\maltese':u'✠', u'\\mapsto':u'↦', u'\\mathcircumflex':u'^', u'\\max':u'max', u'\\measuredangle':u'∡', u'\\mercury':u'☿', u'\\mho':u'℧', u'\\mid':u'∣', u'\\min':u'min', u'\\models':u'⊨', u'\\mp':u'∓', u'\\nabla':u'∇', u'\\natural':u'♮', u'\\ne':u' ≠ ', u'\\nearrow':u'↗', u'\\neg':u'¬', u'\\neg)':u'¬', u'\\neptune':u'♆', u'\\neq':u' ≠ ', u'\\nexists':u'∄', u'\\ni':u'∋', u'\\ni)':u'∋', u'\\nmid':u'∤', u'\\nonumber':u'', u'\\not':u'¬', u'\\not<':u'≮', u'\\not=':u'≠', u'\\not>':u'≯', u'\\not\\in':u' ∉ ', u'\\notin':u'∉', u'\\nparallel':u'∦', u'\\nwarrow':u'↖', u'\\odot':u'⊙', u'\\oint':u'∮', u'\\ominus':u'⊖', u'\\oplus':u'⊕', u'\\oslash':u'⊘', u'\\otimes':u'⊗', u'\\owns':u'∋', u'\\parallel':u'∥', u'\\partial':u'∂', u'\\perp':u'⊥', u'\\pisces':u'♓', u'\\pluto':u'♇', u'\\pm':u'±', u'\\pounds':u'£', u'\\prec':u'≺', u'\\preceq':u'≼', u'\\prime':u'′', u'\\prod':u'', u'\\prompto':u'∝', u'\\propto':u' ∝ ', u'\\qquad':u' ', u'\\quad':u' ', u'\\quarternote':u'♩', u'\\rangle':u'⟩', u'\\rbrace':u'}', u'\\rbrace)':u'}', u'\\rbrack':u']', u'\\rceil':u'⌉', u'\\rfloor':u'⌋', u'\\rhd':u'⊳', u'\\rightarrow':u' → ', u'\\rightarrow)':u'→', u'\\rightharpoondown':u'⇁', u'\\rightharpoonup':u'⇀', u'\\rightharpooondown':u'⇁', u'\\rightharpooonup':u'⇀', u'\\rightleftharpoons':u'⇌', u'\\rightmoon':u'☽', u'\\rightsquigarrow':u' ⇝ ', u'\\sagittarius':u'♐', u'\\saturn':u'♄', u'\\scorpio':u'♏', u'\\scriptscriptstyle':u'', u'\\scriptstyle':u'', u'\\searrow':u'↘', u'\\sec':u'sec', u'\\setminus':u'∖', u'\\sharp':u'♯', u'\\sim':u' ~ ', u'\\simeq':u'≃', u'\\sin':u'sin', u'\\sinh':u'sinh', u'\\slash':u'∕', u'\\smile':u'⌣', u'\\smiley':u'☺', u'\\spadesuit':u'♠', u'\\sphericalangle':u'∢', u'\\sqcap':u'⊓', u'\\sqcup':u'⊔', u'\\sqsubset':u'⊏', u'\\sqsubseteq':u'⊑', u'\\sqsupset':u'⊐', u'\\sqsupseteq':u'⊒', u'\\square':u'□', u'\\star':u'⋆', u'\\subset':u' ⊂ ', u'\\subseteq':u'⊆', u'\\succ':u'≻', u'\\succeq':u'≽', u'\\sum':u'', u'\\sun':u'☼', u'\\sup':u'sup', u'\\supset':u' ⊃ ', u'\\supseteq':u'⊇', u'\\surd':u'√', u'\\swarrow':u'↙', u'\\tan':u'tan', u'\\tanh':u'tanh', u'\\taurus':u'♉', u'\\textbackslash':u'\\', u'\\textstyle':u'', u'\\times':u' × ', u'\\to':u'→', u'\\top':u'⊤', u'\\triangle':u'△', u'\\triangleleft':u'⊲', u'\\triangleright':u'▷', u'\\twonotes':u'♫', u'\\unlhd':u'⊴', u'\\unrhd':u'⊵', u'\\unrhl':u'⊵', u'\\uparrow':u'↑', u'\\updownarrow':u'↕', u'\\uplus':u'⊎', u'\\uranus':u'♅', u'\\varclubsuit':u'♧', u'\\vardiamondsuit':u'♦', u'\\varheartsuit':u'♥', u'\\varnothing':u'∅', u'\\varspadesuit':u'♤', u'\\vdash':u'⊢', u'\\vdots':u'⋮', u'\\vee':u'∨', u'\\vee)':u'∨', u'\\vert':u'∣', u'\\virgo':u'♍', u'\\wedge':u'∧', u'\\wedge)':u'∧', u'\\wp':u'℘', u'\\wr':u'≀', u'\\yen':u'¥', u'\\{':u'{', u'\\|':u'∥', u'\\}':u'}', } decoratingfunctions = { u'\\acute':u'´', u'\\acute{A}':u'Á', u'\\acute{C}':u'Ć', u'\\acute{E}':u'É', u'\\acute{G}':u'Ǵ', u'\\acute{I}':u'Í', u'\\acute{K}':u'Ḱ', u'\\acute{L}':u'Ĺ', u'\\acute{M}':u'Ḿ', u'\\acute{N}':u'Ń', u'\\acute{O}':u'Ó', u'\\acute{P}':u'Ṕ', u'\\acute{R}':u'Ŕ', u'\\acute{S}':u'Ś', u'\\acute{U}':u'Ú', u'\\acute{W}':u'Ẃ', u'\\acute{Y}':u'Ý', u'\\acute{Z}':u'Ź', u'\\acute{a}':u'á', u'\\acute{c}':u'ć', u'\\acute{e}':u'é', u'\\acute{g}':u'ǵ', u'\\acute{i}':u'í', u'\\acute{k}':u'ḱ', u'\\acute{l}':u'ĺ', u'\\acute{m}':u'ḿ', u'\\acute{n}':u'ń', u'\\acute{o}':u'ó', u'\\acute{p}':u'ṕ', u'\\acute{r}':u'ŕ', u'\\acute{s}':u'ś', u'\\acute{u}':u'ú', u'\\acute{w}':u'ẃ', u'\\acute{y}':u'ý', u'\\acute{z}':u'ź', u'\\bar{A}':u'Ā', u'\\bar{E}':u'Ē', u'\\bar{I}':u'Ī', u'\\bar{O}':u'Ō', u'\\bar{U}':u'Ū', u'\\bar{Y}':u'Ȳ', u'\\bar{a}':u'ā', u'\\bar{e}':u'ē', u'\\bar{o}':u'ō', u'\\bar{u}':u'ū', u'\\bar{y}':u'ȳ', u'\\breve':u'˘', u'\\breve{A}':u'Ă', u'\\breve{E}':u'Ĕ', u'\\breve{G}':u'Ğ', u'\\breve{I}':u'Ĭ', u'\\breve{O}':u'Ŏ', u'\\breve{U}':u'Ŭ', u'\\breve{a}':u'ă', u'\\breve{e}':u'ĕ', u'\\breve{g}':u'ğ', u'\\breve{o}':u'ŏ', u'\\breve{u}':u'ŭ', u'\\c':u'¸', u'\\check':u'ˇ', u'\\check{A}':u'Ǎ', u'\\check{C}':u'Č', u'\\check{D}':u'Ď', u'\\check{E}':u'Ě', u'\\check{G}':u'Ǧ', u'\\check{H}':u'Ȟ', u'\\check{I}':u'Ǐ', u'\\check{K}':u'Ǩ', u'\\check{N}':u'Ň', u'\\check{O}':u'Ǒ', u'\\check{R}':u'Ř', u'\\check{S}':u'Š', u'\\check{T}':u'Ť', u'\\check{U}':u'Ǔ', u'\\check{Z}':u'Ž', u'\\check{a}':u'ǎ', u'\\check{c}':u'č', u'\\check{d}':u'ď', u'\\check{e}':u'ě', u'\\check{g}':u'ǧ', u'\\check{h}':u'ȟ', u'\\check{k}':u'ǩ', u'\\check{n}':u'ň', u'\\check{o}':u'ǒ', u'\\check{r}':u'ř', u'\\check{s}':u'š', u'\\check{u}':u'ǔ', u'\\check{z}':u'ž', u'\\c{C}':u'Ç', u'\\c{D}':u'Ḑ', u'\\c{E}':u'Ȩ', u'\\c{G}':u'Ģ', u'\\c{H}':u'Ḩ', u'\\c{K}':u'Ķ', u'\\c{L}':u'Ļ', u'\\c{N}':u'Ņ', u'\\c{R}':u'Ŗ', u'\\c{S}':u'Ş', u'\\c{T}':u'Ţ', u'\\c{c}':u'ç', u'\\c{d}':u'ḑ', u'\\c{e}':u'ȩ', u'\\c{h}':u'ḩ', u'\\c{k}':u'ķ', u'\\c{l}':u'ļ', u'\\c{n}':u'ņ', u'\\c{r}':u'ŗ', u'\\c{s}':u'ş', u'\\c{t}':u'ţ', u'\\dacute{O}':u'Ő', u'\\dacute{U}':u'Ű', u'\\dacute{o}':u'ő', u'\\dacute{u}':u'ű', u'\\ddot':u'¨', u'\\ddot{A}':u'Ä', u'\\ddot{E}':u'Ë', u'\\ddot{H}':u'Ḧ', u'\\ddot{I}':u'Ï', u'\\ddot{O}':u'Ö', u'\\ddot{U}':u'Ü', u'\\ddot{W}':u'Ẅ', u'\\ddot{X}':u'Ẍ', u'\\ddot{Y}':u'Ÿ', u'\\ddot{a}':u'ä', u'\\ddot{e}':u'ë', u'\\ddot{h}':u'ḧ', u'\\ddot{o}':u'ö', u'\\ddot{t}':u'ẗ', u'\\ddot{u}':u'ü', u'\\ddot{w}':u'ẅ', u'\\ddot{x}':u'ẍ', u'\\ddot{y}':u'ÿ', u'\\dgrave{A}':u'Ȁ', u'\\dgrave{E}':u'Ȅ', u'\\dgrave{I}':u'Ȉ', u'\\dgrave{O}':u'Ȍ', u'\\dgrave{R}':u'Ȑ', u'\\dgrave{U}':u'Ȕ', u'\\dgrave{a}':u'ȁ', u'\\dgrave{e}':u'ȅ', u'\\dgrave{o}':u'ȍ', u'\\dgrave{r}':u'ȑ', u'\\dgrave{u}':u'ȕ', u'\\dot':u'˙', u'\\dot{A}':u'Ȧ', u'\\dot{B}':u'Ḃ', u'\\dot{C}':u'Ċ', u'\\dot{D}':u'Ḋ', u'\\dot{E}':u'Ė', u'\\dot{F}':u'Ḟ', u'\\dot{G}':u'Ġ', u'\\dot{H}':u'Ḣ', u'\\dot{I}':u'İ', u'\\dot{M}':u'Ṁ', u'\\dot{N}':u'Ṅ', u'\\dot{O}':u'Ȯ', u'\\dot{P}':u'Ṗ', u'\\dot{R}':u'Ṙ', u'\\dot{S}':u'Ṡ', u'\\dot{T}':u'Ṫ', u'\\dot{W}':u'Ẇ', u'\\dot{X}':u'Ẋ', u'\\dot{Y}':u'Ẏ', u'\\dot{Z}':u'Ż', u'\\dot{a}':u'ȧ', u'\\dot{b}':u'ḃ', u'\\dot{c}':u'ċ', u'\\dot{d}':u'ḋ', u'\\dot{e}':u'ė', u'\\dot{f}':u'ḟ', u'\\dot{g}':u'ġ', u'\\dot{h}':u'ḣ', u'\\dot{m}':u'ṁ', u'\\dot{n}':u'ṅ', u'\\dot{o}':u'ȯ', u'\\dot{p}':u'ṗ', u'\\dot{r}':u'ṙ', u'\\dot{s}':u'ṡ', u'\\dot{t}':u'ṫ', u'\\dot{w}':u'ẇ', u'\\dot{x}':u'ẋ', u'\\dot{y}':u'ẏ', u'\\dot{z}':u'ż', u'\\grave':u'`', u'\\grave{A}':u'À', u'\\grave{E}':u'È', u'\\grave{I}':u'Ì', u'\\grave{N}':u'Ǹ', u'\\grave{O}':u'Ò', u'\\grave{U}':u'Ù', u'\\grave{W}':u'Ẁ', u'\\grave{Y}':u'Ỳ', u'\\grave{a}':u'à', u'\\grave{e}':u'è', u'\\grave{n}':u'ǹ', u'\\grave{o}':u'ò', u'\\grave{u}':u'ù', u'\\grave{w}':u'ẁ', u'\\grave{y}':u'ỳ', u'\\hat':u'^', u'\\hat{A}':u'Â', u'\\hat{C}':u'Ĉ', u'\\hat{E}':u'Ê', u'\\hat{G}':u'Ĝ', u'\\hat{H}':u'Ĥ', u'\\hat{I}':u'Î', u'\\hat{J}':u'Ĵ', u'\\hat{O}':u'Ô', u'\\hat{S}':u'Ŝ', u'\\hat{U}':u'Û', u'\\hat{W}':u'Ŵ', u'\\hat{Y}':u'Ŷ', u'\\hat{Z}':u'Ẑ', u'\\hat{a}':u'â', u'\\hat{c}':u'ĉ', u'\\hat{e}':u'ê', u'\\hat{g}':u'ĝ', u'\\hat{h}':u'ĥ', u'\\hat{o}':u'ô', u'\\hat{s}':u'ŝ', u'\\hat{u}':u'û', u'\\hat{w}':u'ŵ', u'\\hat{y}':u'ŷ', u'\\hat{z}':u'ẑ', u'\\mathring':u'°', u'\\ogonek{A}':u'Ą', u'\\ogonek{E}':u'Ę', u'\\ogonek{I}':u'Į', u'\\ogonek{O}':u'Ǫ', u'\\ogonek{U}':u'Ų', u'\\ogonek{a}':u'ą', u'\\ogonek{e}':u'ę', u'\\ogonek{i}':u'į', u'\\ogonek{o}':u'ǫ', u'\\ogonek{u}':u'ų', u'\\overleftarrow':u'⟵', u'\\overrightarrow':u'⟶', u'\\rcap{A}':u'Ȃ', u'\\rcap{E}':u'Ȇ', u'\\rcap{I}':u'Ȋ', u'\\rcap{O}':u'Ȏ', u'\\rcap{R}':u'Ȓ', u'\\rcap{U}':u'Ȗ', u'\\rcap{a}':u'ȃ', u'\\rcap{e}':u'ȇ', u'\\rcap{o}':u'ȏ', u'\\rcap{r}':u'ȓ', u'\\rcap{u}':u'ȗ', u'\\slashed{O}':u'Ø', u'\\slashed{o}':u'ø', u'\\subdot{A}':u'Ạ', u'\\subdot{B}':u'Ḅ', u'\\subdot{D}':u'Ḍ', u'\\subdot{E}':u'Ẹ', u'\\subdot{H}':u'Ḥ', u'\\subdot{I}':u'Ị', u'\\subdot{K}':u'Ḳ', u'\\subdot{L}':u'Ḷ', u'\\subdot{M}':u'Ṃ', u'\\subdot{N}':u'Ṇ', u'\\subdot{O}':u'Ọ', u'\\subdot{R}':u'Ṛ', u'\\subdot{S}':u'Ṣ', u'\\subdot{T}':u'Ṭ', u'\\subdot{U}':u'Ụ', u'\\subdot{V}':u'Ṿ', u'\\subdot{W}':u'Ẉ', u'\\subdot{Y}':u'Ỵ', u'\\subdot{Z}':u'Ẓ', u'\\subdot{a}':u'ạ', u'\\subdot{b}':u'ḅ', u'\\subdot{d}':u'ḍ', u'\\subdot{e}':u'ẹ', u'\\subdot{h}':u'ḥ', u'\\subdot{i}':u'ị', u'\\subdot{k}':u'ḳ', u'\\subdot{l}':u'ḷ', u'\\subdot{m}':u'ṃ', u'\\subdot{n}':u'ṇ', u'\\subdot{o}':u'ọ', u'\\subdot{r}':u'ṛ', u'\\subdot{s}':u'ṣ', u'\\subdot{t}':u'ṭ', u'\\subdot{u}':u'ụ', u'\\subdot{v}':u'ṿ', u'\\subdot{w}':u'ẉ', u'\\subdot{y}':u'ỵ', u'\\subdot{z}':u'ẓ', u'\\subhat{D}':u'Ḓ', u'\\subhat{E}':u'Ḙ', u'\\subhat{L}':u'Ḽ', u'\\subhat{N}':u'Ṋ', u'\\subhat{T}':u'Ṱ', u'\\subhat{U}':u'Ṷ', u'\\subhat{d}':u'ḓ', u'\\subhat{e}':u'ḙ', u'\\subhat{l}':u'ḽ', u'\\subhat{n}':u'ṋ', u'\\subhat{t}':u'ṱ', u'\\subhat{u}':u'ṷ', u'\\subring{A}':u'Ḁ', u'\\subring{a}':u'ḁ', u'\\subtilde{E}':u'Ḛ', u'\\subtilde{I}':u'Ḭ', u'\\subtilde{U}':u'Ṵ', u'\\subtilde{e}':u'ḛ', u'\\subtilde{i}':u'ḭ', u'\\subtilde{u}':u'ṵ', u'\\tilde':u'˜', u'\\tilde{A}':u'Ã', u'\\tilde{E}':u'Ẽ', u'\\tilde{I}':u'Ĩ', u'\\tilde{N}':u'Ñ', u'\\tilde{O}':u'Õ', u'\\tilde{U}':u'Ũ', u'\\tilde{V}':u'Ṽ', u'\\tilde{Y}':u'Ỹ', u'\\tilde{a}':u'ã', u'\\tilde{e}':u'ẽ', u'\\tilde{n}':u'ñ', u'\\tilde{o}':u'õ', u'\\tilde{u}':u'ũ', u'\\tilde{v}':u'ṽ', u'\\tilde{y}':u'ỹ', u'\\vec':u'→', } endings = { u'bracket':u'}', u'complex':u'\\]', u'endafter':u'}', u'endbefore':u'\\end{', u'squarebracket':u']', } fontfunctions = { u'\\boldsymbol':u'b', u'\\mathbb':u'span class="blackboard"', u'\\mathbb{A}':u'𝔸', u'\\mathbb{B}':u'𝔹', u'\\mathbb{C}':u'ℂ', u'\\mathbb{D}':u'𝔻', u'\\mathbb{E}':u'𝔼', u'\\mathbb{F}':u'𝔽', u'\\mathbb{G}':u'𝔾', u'\\mathbb{H}':u'ℍ', u'\\mathbb{J}':u'𝕁', u'\\mathbb{K}':u'𝕂', u'\\mathbb{L}':u'𝕃', u'\\mathbb{N}':u'ℕ', u'\\mathbb{O}':u'𝕆', u'\\mathbb{P}':u'ℙ', u'\\mathbb{Q}':u'ℚ', u'\\mathbb{R}':u'ℝ', u'\\mathbb{S}':u'𝕊', u'\\mathbb{T}':u'𝕋', u'\\mathbb{W}':u'𝕎', u'\\mathbb{Z}':u'ℤ', u'\\mathbf':u'b', u'\\mathcal':u'span class="script"', u'\\mathfrak':u'span class="fraktur"', u'\\mathfrak{C}':u'ℭ', u'\\mathfrak{F}':u'𝔉', u'\\mathfrak{H}':u'ℌ', u'\\mathfrak{I}':u'ℑ', u'\\mathfrak{R}':u'ℜ', u'\\mathfrak{Z}':u'ℨ', u'\\mathit':u'i', u'\\mathring{A}':u'Å', u'\\mathring{U}':u'Ů', u'\\mathring{a}':u'å', u'\\mathring{u}':u'ů', u'\\mathring{w}':u'ẘ', u'\\mathring{y}':u'ẙ', u'\\mathrm':u'span class="mathrm"', u'\\mathscr':u'span class="script"', u'\\mathscr{B}':u'ℬ', u'\\mathscr{E}':u'ℰ', u'\\mathscr{F}':u'ℱ', u'\\mathscr{H}':u'ℋ', u'\\mathscr{I}':u'ℐ', u'\\mathscr{L}':u'ℒ', u'\\mathscr{M}':u'ℳ', u'\\mathscr{R}':u'ℛ', u'\\mathsf':u'span class="mathsf"', u'\\mathtt':u'tt', } hybridfunctions = { u'\\binom':[u'{$1}{$2}',u'f3{(}f0{f1{$1}f2{$2}}f3{)}',u'span class="binom"',u'span class="upbinom"',u'span class="downbinom"',u'span class="bigsymbol"',], u'\\cfrac':[u'[$p]{$1}{$2}',u'f0{f1{$1}f2{$2}}',u'span class="fullfraction"',u'span class="numerator$p"',u'span class="denominator"',], u'\\dbinom':[u'{$1}{$2}',u'f3{(}f0{f1{$1}f2{$2}}f3{)}',u'span class="fullbinom"',u'span class="upbinom"',u'span class="downbinom"',u'span class="bigsymbol"',], u'\\dfrac':[u'{$1}{$2}',u'f0{f1{$1}f2{$2}}',u'span class="fullfraction"',u'span class="numerator"',u'span class="denominator"',], u'\\frac':[u'{$1}{$2}',u'f0{f1{$1}f2{$2}}',u'span class="fraction"',u'span class="numerator"',u'span class="denominator"',], u'\\hspace':[u'{$p}',u'f0{ }',u'span class="hspace" style="width: $p;"',], u'\\leftroot':[u'{$p}',u'f0{ }',u'span class="leftroot" style="width: $p;px"',], u'\\nicefrac':[u'{$1}{$2}',u'f0{f1{$1}⁄f2{$2}}',u'span class="fraction"',u'sup class="numerator"',u'sub class="denominator"',], u'\\raisebox':[u'{$p}{$1}',u'f0{$1}',u'span class="raisebox" style="vertical-align: $p;"',], u'\\sqrt':[u'[$0]{$1}',u'f1{$0}f0{f2{√}f3{$1}}',u'span class="sqrt"',u'sup',u'span class="radical"',u'span class="root"',], u'\\tbinom':[u'{$1}{$2}',u'f3{(}f0{f1{$1}f2{$2}}f3{)}',u'span class="fullbinom"',u'span class="upbinom"',u'span class="downbinom"',u'span class="bigsymbol"',], u'\\unit':[u'[$0]{$1}',u'$0f0{$1.font}',u'span class="unit"',], u'\\unitfrac':[u'[$0]{$1}{$2}',u'$0f0{f1{$1.font}⁄f2{$2.font}}',u'span class="fraction"',u'sup class="unit"',u'sub class="unit"',], u'\\uproot':[u'{$p}',u'f0{ }',u'span class="uproot" style="width: $p;px"',], u'\\vspace':[u'{$p}',u'f0{ }',u'span class="vspace" style="height: $p;"',], } labelfunctions = { u'\\label':u'a name="#"', } limits = { u'commands':[u'\\sum',u'\\int',u'\\intop',], u'operands':[u'^',u'_',], } modified = { u'\n':u'', u' ':u'', u'&':u' ', u'\'':u'’', u'+':u' + ', u',':u', ', u'-':u' − ', u'/':u' ⁄ ', u'<':u' < ', u'=':u' = ', u'>':u' > ', u'@':u'', u'~':u'', } onefunctions = { u'\\Big':u'span class="bigsymbol"', u'\\Bigg':u'span class="hugesymbol"', u'\\bar':u'span class="bar"', u'\\begin{array}':u'span class="arraydef"', u'\\big':u'span class="symbol"', u'\\bigg':u'span class="largesymbol"', u'\\bigl':u'span class="bigsymbol"', u'\\bigr':u'span class="bigsymbol"', u'\\hphantom':u'span class="phantom"', u'\\left':u'span class="symbol"', u'\\left.':u'', u'\\middle':u'span class="symbol"', u'\\overline':u'span class="overline"', u'\\phantom':u'span class="phantom"', u'\\right':u'span class="symbol"', u'\\right.':u'', u'\\underline':u'u', u'\\vphantom':u'span class="phantom"', } starts = { u'beginafter':u'}', u'beginbefore':u'\\begin{', u'bracket':u'{', u'command':u'\\', u'complex':u'\\[', u'simple':u'$', u'squarebracket':u'[', u'unnumbered':u'*', } symbolfunctions = { u'^':u'sup', u'_':u'sub', } textfunctions = { u'\\mbox':u'span class="mbox"', u'\\text':u'span class="text"', u'\\textbf':u'b', u'\\textipa':u'span class="textipa"', u'\\textit':u'i', u'\\textrm':u'span class="mathrm"', u'\\textsc':u'span class="versalitas"', u'\\textsf':u'span class="mathsf"', u'\\textsl':u'i', u'\\texttt':u'tt', } unmodified = { u'characters':[u'.',u'*',u'€',u'(',u')',u'[',u']',u':',u'·',u'!',u';',u'|',u'§',u'"',], } class GeneralConfig(object): "Configuration class from config file" version = { u'date':u'2009-12-16', u'lyxformat':u'345', u'number':u'0.39', } class HeaderConfig(object): "Configuration class from config file" parameters = { u'branch':u'\\branch', u'documentclass':u'\\textclass', u'endbranch':u'\\end_branch', u'paragraphseparation':u'\\paragraph_separation', u'pdftitle':u'\\pdf_title', u'secnumdepth':u'\\secnumdepth', u'tocdepth':u'\\tocdepth', } styles = { 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',], } class ImageConfig(object): "Configuration class from config file" formats = { u'default':u'.png', u'raster':[u'.png',u'.jpg',], u'vector':[u'.svg',u'.eps',], } size = { u'ignoredtexts':[u'col',u'text',u'line',u'page',u'theight',u'pheight',], } class NumberingConfig(object): "Configuration class from config file" layouts = { u'ordered':[u'Chapter',u'Section',u'Subsection',u'Subsubsection',u'Paragraph',], u'unique':[u'Part',u'Book',], } class StyleConfig(object): "Configuration class from config file" quotes = { u'ald':u'»', u'als':u'›', u'ard':u'«', u'ars':u'‹', u'eld':u'“', u'els':u'‘', u'erd':u'”', u'ers':u'’', u'fld':u'«', u'fls':u'‹', u'frd':u'»', u'frs':u'›', u'gld':u'„', u'gls':u'‚', u'grd':u'“', u'grs':u'‘', u'pld':u'„', u'pls':u'‚', u'prd':u'”', u'prs':u'’', u'sld':u'”', u'srd':u'”', } spaces = { u'\\enskip{}':u' ', u'\\hfill{}':u' ', u'\\hspace*{\\fill}':u' ', u'\\hspace*{}':u'', u'\\hspace{}':u' ', u'\\negthinspace{}':u'', u'\\qquad{}':u'  ', u'\\quad{}':u' ', u'\\space{}':u' ', u'\\thinspace{}':u' ', u'~':u' ', } class TagConfig(object): "Configuration class from config file" barred = { u'under':u'u', } boxes = { u'Framed':u'div class="framed"', u'Frameless':u'div class="frameless"', } family = { u'sans':u'span class="sans"', u'typewriter':u'tt', } layouts = { u'Center':u'div', u'Chapter':u'h?', u'Date':u'h2', u'LyX-Code':u'pre', u'Paragraph':u'div', u'Part':u'h1', u'Quotation':u'blockquote', u'Quote':u'blockquote', u'Section':u'h?', u'Subsection':u'h?', u'Subsubsection':u'h?', } listitems = { u'Enumerate':u'ol', u'Itemize':u'ul', } notes = { u'Comment':u'', u'Greyedout':u'span class="greyedout"', u'Note':u'', } shaped = { u'italic':u'i', u'slanted':u'i', u'smallcaps':u'span class="versalitas"', } class TranslationConfig(object): "Configuration class from config file" constants = { u'Book':u'Book', u'Chapter':u'Chapter', u'Paragraph':u'Paragraph', u'Part':u'Part', u'Section':u'Section', u'Subsection':u'Subsection', u'Subsubsection':u'Subsubsection', u'abstract':u'Abstract', u'bibliography':u'Bibliography', u'figure':u'figure', u'index':u'Index', u'nomenclature':u'Nomenclature', u'toc':u'Table of Contents', } floats = { u'algorithm':u'Listing ', u'figure':u'Figure ', u'listing':u'Listing ', u'table':u'Table ', u'tableau':u'Tableau ', } lists = { u'algorithm':u'List of Listings', u'figure':u'List of Figures', u'table':u'List of Tables', u'tableau':u'List of Tableaux', } class CommandLineParser(object): "A parser for runtime options" def __init__(self, options): self.options = options def parseoptions(self, args): "Parse command line options" if len(args) == 0: return None while len(args) > 0 and args[0].startswith('--'): key, value = self.readoption(args) if not key: return 'Option ' + value + ' not recognized' if not value: return 'Option ' + key + ' needs a value' setattr(self.options, key, value) return None def readoption(self, args): "Read the key and value for an option" arg = args[0][2:] del args[0] if '=' in arg: return self.readequals(arg, args) key = arg if not hasattr(self.options, key): return None, key current = getattr(self.options, key) if current.__class__ == bool: return key, True # read value if len(args) == 0: return key, None if args[0].startswith('"'): initial = args[0] del args[0] return key, self.readquoted(args, initial) value = args[0] del args[0] return key, value def readquoted(self, args, initial): "Read a value between quotes" value = initial[1:] while len(args) > 0 and not args[0].endswith('"') and not args[0].startswith('--'): value += ' ' + args[0] del args[0] if len(args) == 0 or args[0].startswith('--'): return None value += ' ' + args[0:-1] return value def readequals(self, arg, args): "Read a value with equals" split = arg.split('=', 1) key = split[0] value = split[1] if not value.startswith('"'): return key, value return key, self.readquoted(args, value) class Options(object): "A set of runtime options" instance = None nocopy = False debug = False quiet = False version = False hardversion = False versiondate = False html = False help = False showlines = True unicode = False css = 'http://www.nongnu.org/elyxer/lyx.css' title = None directory = None destdirectory = None toc = False toctarget = '' forceformat = None lyxformat = False target = None splitpart = None memory = True lowmem = False branches = dict() def __init__(self): self.invokedas = None def parseoptions(self, args): "Parse command line options" self.invokedas = os.path.basename(args[0]) del args[0] parser = CommandLineParser(Options) result = parser.parseoptions(args) if result: Trace.error(result) self.usage() if Options.help: self.usage() if Options.version: self.showversion() if Options.hardversion: self.showhardversion() if Options.versiondate: self.showversiondate() if Options.lyxformat: self.showlyxformat() if Options.splitpart: try: Options.splitpart = int(Options.splitpart) if Options.splitpart <= 0: Trace.error('--splitpart requires a number bigger than zero') self.usage() except: Trace.error('--splitpart needs a numeric argument, not ' + Options.splitpart) self.usage() if Options.lowmem or Options.toc: Options.memory = False # set in Trace if necessary for param in dir(Options): if hasattr(Trace, param + 'mode'): setattr(Trace, param + 'mode', getattr(self, param)) def usage(self): "Show correct usage" Trace.error('Usage: ' + self.invokedas + ' [options] [filein] [fileout]') Trace.error('Convert LyX input file "filein" to HTML file "fileout".') Trace.error('If filein (or fileout) is not given use standard input (or output).') self.showoptions() def showoptions(self): "Show all possible options" Trace.error(' Valid options:') Trace.error(' --nocopy: disables the copyright notice at the bottom') Trace.error(' --quiet: disables all runtime messages') Trace.error(' --debug: enable debugging messages (for developers)') Trace.error(' --title "title": set the generated page title') Trace.error(' --directory "img_dir": look for images in the specified directory') Trace.error(' --destdirectory "dest": put converted images into this directory') Trace.error(' --css "file.css": use a custom CSS file') Trace.error(' --version: show version number and release date') Trace.error(' --html: output HTML 4.0 instead of the default XHTML') Trace.error(' --unicode: full Unicode output') Trace.error(' --forceformat ".ext": force image output format') Trace.error(' --lyxformat: return the highest LyX version that can be') Trace.error(' converted') Trace.error(' --toc: create a table of contents') Trace.error(' --target "frame": make all links point to the given frame') Trace.error(' --lowmem: do the conversion on the fly (conserve memory)') exit() def showversion(self): "Return the current eLyXer version string" string = 'eLyXer version ' + GeneralConfig.version['number'] string += ' (' + GeneralConfig.version['date'] + ')' Trace.error(string) exit() def showhardversion(self): "Return just the version string" Trace.message(GeneralConfig.version['number']) exit() def showversiondate(self): "Return just the version dte" Trace.message(GeneralConfig.version['date']) exit() def showlyxformat(self): "Return just the lyxformat parameter" Trace.message(GeneralConfig.version['lyxformat']) exit() class BranchOptions(object): "A set of options for a branch" def __init__(self, name): self.name = name self.options = {'color':'#ffffff'} def set(self, key, value): "Set a branch option" if not key.startswith(ContainerConfig.string['startcommand']): Trace.error('Invalid branch option ' + key) return key = key.replace(ContainerConfig.string['startcommand'], '') self.options[key] = value def isselected(self): "Return if the branch is selected" if not 'selected' in self.options: return False return self.options['selected'] == '1' def __unicode__(self): "String representation" return 'options for ' + self.name + ': ' + unicode(self.options) import codecs class Parser(object): "A generic parser" def __init__(self): self.begin = 0 self.parameters = dict() def parseheader(self, reader): "Parse the header" header = reader.currentline().split() reader.nextline() self.begin = reader.linenumber return header def parseparameter(self, reader): "Parse a parameter" if reader.currentline().strip().startswith('<'): key, value = self.parsexml(reader) self.parameters[key] = value return split = reader.currentline().strip().split(' ', 1) reader.nextline() if len(split) == 0: return key = split[0] if len(split) == 1: self.parameters[key] = True return if not '"' in split[1]: self.parameters[key] = split[1].strip() return doublesplit = split[1].split('"') self.parameters[key] = doublesplit[1] def parsexml(self, reader): "Parse a parameter in xml form: " strip = reader.currentline().strip() reader.nextline() if not strip.endswith('>'): Trace.error('XML parameter ' + strip + ' should be <...>') split = strip[1:-1].split() if len(split) == 0: Trace.error('Empty XML parameter <>') return None, None key = split[0] del split[0] if len(split) == 0: return key, dict() attrs = dict() for attr in split: if not '=' in attr: Trace.error('Erroneous attribute ' + attr) attr += '="0"' parts = attr.split('=') attrkey = parts[0] value = parts[1].split('"')[1] attrs[attrkey] = value return key, attrs def parseending(self, reader, process): "Parse until the current ending is found" while not reader.currentline().startswith(self.ending): process() def parsecontainer(self, reader, contents): container = self.factory.createcontainer(reader) if container: container.parent = self.parent contents.append(container) def __unicode__(self): "Return a description" return self.__class__.__name__ + ' (' + unicode(self.begin) + ')' class LoneCommand(Parser): "A parser for just one command line" def parse(self,reader): "Read nothing" return [] class TextParser(Parser): "A parser for a command and a bit of text" stack = [] def __init__(self, ending): Parser.__init__(self) self.ending = ending self.endings = [] def parse(self, reader): "Parse lines as long as they are text" TextParser.stack.append(self.ending) self.endings = TextParser.stack + [ContainerConfig.endings['Layout'], ContainerConfig.endings['Inset'], self.ending] contents = [] while not self.isending(reader): self.parsecontainer(reader, contents) return contents def isending(self, reader): "Check if text is ending" current = reader.currentline().split() if len(current) == 0: return False if current[0] in self.endings: if current[0] in TextParser.stack: TextParser.stack.remove(current[0]) else: TextParser.stack = [] return True return False class ExcludingParser(Parser): "A parser that excludes the final line" def parse(self, reader): "Parse everything up to (and excluding) the final line" contents = [] self.parseending(reader, lambda: self.parsecontainer(reader, contents)) return contents class BoundedParser(ExcludingParser): "A parser bound by a final line" def parse(self, reader): "Parse everything, including the final line" contents = ExcludingParser.parse(self, reader) # skip last line reader.nextline() return contents class BoundedDummy(Parser): "A bound parser that ignores everything" def parse(self, reader): "Parse the contents of the container" self.parseending(reader, lambda: reader.nextline()) # skip last line reader.nextline() return [] class StringParser(Parser): "Parses just a string" def parseheader(self, reader): "Do nothing, just take note" self.begin = reader.linenumber + 1 return [] def parse(self, reader): "Parse a single line" contents = [reader.currentline()] reader.nextline() return contents class InsetParser(BoundedParser): "Parses a LyX inset" def parse(self, reader): "Parse inset parameters into a dictionary" startcommand = ContainerConfig.string['startcommand'] while reader.currentline() != '' and not reader.currentline().startswith(startcommand): self.parseparameter(reader) return BoundedParser.parse(self, reader) class HeaderParser(Parser): "Parses the LyX header" def parse(self, reader): "Parse header parameters into a dictionary" self.parseending(reader, lambda: self.parseline(reader)) # skip last line reader.nextline() return [] def parseline(self, reader): "Parse a single line as a parameter or as a start" line = reader.currentline() if line.startswith(HeaderConfig.parameters['branch']): self.parsebranch(reader) return # no match self.parseparameter(reader) def parsebranch(self, reader): branch = reader.currentline().split()[1] reader.nextline() subparser = HeaderParser().complete(HeaderConfig.parameters['endbranch']) subparser.parse(reader) options = BranchOptions(branch) for key in subparser.parameters: options.set(key, subparser.parameters[key]) Options.branches[branch] = options def complete(self, ending): self.ending = ending return self import codecs import datetime class EmptyOutput(object): "The output for some container" def gethtml(self, container): "Return empty HTML code" return [] class FixedOutput(object): "Fixed output" def gethtml(self, container): "Return constant HTML code" return container.html class ContentsOutput(object): "Outputs the contents converted to HTML" def gethtml(self, container): "Return the HTML code" html = [] if container.contents == None: return html for element in container.contents: if not hasattr(element, 'gethtml'): Trace.error('No html in ' + element.__class__.__name__ + ': ' + unicode(element)) return html html += element.gethtml() return html class TaggedOutput(ContentsOutput): "Outputs an HTML tag surrounding the contents" def __init__(self): self.breaklines = False def settag(self, tag, breaklines=False): "Set the value for the tag" self.tag = tag self.breaklines = breaklines return self def setbreaklines(self, breaklines): "Set the value for breaklines" self.breaklines = breaklines return self def gethtml(self, container): "Return the HTML code" html = [self.getopen(container)] html += ContentsOutput.gethtml(self, container) html.append(self.getclose(container)) return html def getopen(self, container): "Get opening line" if self.tag == '': return '' open = '<' + self.tag + '>' if self.breaklines: return '\n' + open + '\n' return open def getclose(self, container): "Get closing line" if self.tag == '': return '' close = '' if self.breaklines: return '\n' + close return close class StringOutput(object): "Returns a bare string as output" def gethtml(self, container): "Return a bare string" return [container.string] class HeaderOutput(object): "Returns the HTML headers" def gethtml(self, container): "Return a constant header" if not Options.html: html = [u'\n'] html.append(u'\n') html.append(u'\n') else: html = [u'\n'] html.append(u'\n') html.append(u'\n') html.append(u'\n') html.append(u'\n') html.append(u'\n') html.append(u'\n') html += TitleOutput().gethtml(container) html.append('\n') html.append('\n') html.append('
\n') return html class TitleOutput(object): "Return the HTML title tag" pdftitle = None title = None def gethtml(self, container): "Return the title tag" return ['' + self.gettitle() + '\n'] def gettitle(self): "Return the correct title from the option or the PDF title" if Options.title: return Options.title if TitleOutput.title: return TitleOutput.title if TitleOutput.pdftitle: return TitleOutput.pdftitle return 'Converted document' class FooterOutput(object): "Return the HTML code for the footer" author = None def gethtml(self, container): "Footer HTML" html = [] html.append('\n\n') if FooterOutput.author and not Options.nocopy: html.append('
\n') year = datetime.date.today().year html.append('

Copyright (C) ' + unicode(year) + ' ' + FooterOutput.author + '

\n') html.append('
\n') html.append('\n') html.append('\n') return html class Container(object): "A container for text and objects in a lyx file" def __init__(self): self.contents = list() def process(self): "Process contents" pass def gethtml(self): "Get the resulting HTML" html = self.output.gethtml(self) if isinstance(html, basestring): Trace.error('Raw string ' + html) html = [html] if Options.html: self.escapeall(html, EscapeConfig.html) if not Options.unicode: self.escapeall(html, EscapeConfig.nonunicode) return html def escapeall(self, lines, replacements): "Escape all lines in an array with the replacements" for index, line in enumerate(lines): lines[index] = self.escape(line, replacements) def escape(self, line, replacements = EscapeConfig.entities): "Escape a line with replacements from a map" pieces = replacements.keys() # do them in order pieces.sort() for piece in pieces: if piece in line: line = line.replace(piece, replacements[piece]) return line def searchall(self, type): "Search for all embedded containers of a given type" list = [] self.searchprocess(type, lambda container: list.append(container)) return list def searchremove(self, type): "Search for all containers of a type and remove them" list = [] self.searchprocess(type, lambda container: self.appendremove(list, container)) return list def appendremove(self, list, container): "Append to a list and remove from own contents" list.append(container) container.parent.contents.remove(container) def searchprocess(self, type, process): "Search for elements of a given type and process them" self.locateprocess(lambda container: isinstance(container, type), process) def locateprocess(self, locate, process): "Search for all embedded containers and process them" for container in self.contents: container.locateprocess(locate, process) if locate(container): process(container) def extracttext(self): "Search for all the strings and extract the text they contain" text = '' strings = self.searchall(StringContainer) for string in strings: text += string.string return text def group(self, index, group, isingroup): "Group some adjoining elements into a group" if index >= len(self.contents): return if hasattr(self.contents[index], 'grouped'): return while index < len(self.contents) and isingroup(self.contents[index]): self.contents[index].grouped = True group.contents.append(self.contents[index]) self.contents.pop(index) self.contents.insert(index, group) def remove(self, index): "Remove a container but leave its contents" container = self.contents[index] self.contents.pop(index) while len(container.contents) > 0: self.contents.insert(index, container.contents.pop()) def debug(self, level = 0): "Show the contents in debug mode" if not Trace.debugmode: return Trace.debug(' ' * level + unicode(self)) for element in self.contents: element.debug(level + 1) def parselstparams(self): "Parse a multiple parameter lstparams." if not 'lstparams' in self.parameters: return paramlist = self.parameters['lstparams'].split(',') for param in paramlist: if not '=' in param: Trace.error('Invalid listing parameter ' + param) else: key, value = param.split('=', 1) self.parameters[key] = value def tree(self, level = 0): "Show in a tree" Trace.debug(" " * level + unicode(self)) for container in self.contents: container.tree(level + 1) def __unicode__(self): "Get a description" if not hasattr(self, 'begin'): return self.__class__.__name__ return self.__class__.__name__ + '@' + unicode(self.begin) class BlackBox(Container): "A container that does not output anything" def __init__(self): self.parser = LoneCommand() self.output = EmptyOutput() self.contents = [] class LyXFormat(BlackBox): "Read the lyxformat command" def process(self): "Show warning if version < 276" version = int(self.header[1]) if version < 276: Trace.error('Warning: unsupported format version ' + str(version)) class StringContainer(Container): "A container for a single string" def __init__(self): self.parser = StringParser() self.output = StringOutput() self.string = '' def process(self): "Replace special chars from the contents." self.string = self.replacespecial(self.contents[0]) self.contents = [] def replacespecial(self, line): "Replace all special chars from a line" replaced = self.escape(line, EscapeConfig.entities) replaced = self.changeline(replaced) if ContainerConfig.string['startcommand'] in replaced and len(replaced) > 1: # unprocessed commands message = 'Unknown command at ' + unicode(self.parser.begin) + ': ' Trace.error(message + replaced.strip()) return replaced def changeline(self, line): line = self.escape(line, EscapeConfig.chars) if not ContainerConfig.string['startcommand'] in line: return line line = self.escape(line, EscapeConfig.commands) return line def __unicode__(self): result = 'StringContainer@' + unicode(self.begin) return result + ' (' + self.string.strip()[:15] + '...)' class Constant(StringContainer): "A constant string" def __init__(self, text): self.contents = [] self.string = text self.output = StringOutput() def __unicode__(self): return 'Constant: ' + self.string class TaggedText(Container): "Text inside a tag" def __init__(self): ending = None if self.__class__.__name__ in ContainerConfig.endings: ending = ContainerConfig.endings[self.__class__.__name__] self.parser = TextParser(ending) self.output = TaggedOutput() def complete(self, contents, tag, breaklines=False): "Complete the tagged text and return it" self.contents = contents self.output.tag = tag self.output.breaklines = breaklines return self def constant(self, text, tag, breaklines=False): "Complete the tagged text with a constant" constant = Constant(text) return self.complete([constant], tag, breaklines) def __unicode__(self): return 'Tagged <' + self.output.tag + '>' class QuoteContainer(Container): "A container for a pretty quote" def __init__(self): self.parser = BoundedParser() self.output = FixedOutput() def process(self): "Process contents" self.type = self.header[2] if not self.type in StyleConfig.quotes: Trace.error('Quote type ' + self.type + ' not found') self.html = ['"'] return self.html = [StyleConfig.quotes[self.type]] class LyXLine(Container): "A Lyx line" def __init__(self): self.parser = LoneCommand() self.output = FixedOutput() def process(self): self.html = ['
'] class EmphaticText(TaggedText): "Text with emphatic mode" def process(self): self.output.tag = 'i' class ShapedText(TaggedText): "Text shaped (italic, slanted)" def process(self): self.type = self.header[1] if not self.type in TagConfig.shaped: Trace.error('Unrecognized shape ' + self.header[1]) self.output.tag = 'span' return self.output.tag = TagConfig.shaped[self.type] class VersalitasText(TaggedText): "Text in versalitas" def process(self): self.output.tag = 'span class="versalitas"' class ColorText(TaggedText): "Colored text" def process(self): self.color = self.header[1] self.output.tag = 'span class="' + self.color + '"' class SizeText(TaggedText): "Sized text" def process(self): self.size = self.header[1] self.output.tag = 'span class="' + self.size + '"' class BoldText(TaggedText): "Bold text" def process(self): self.output.tag = 'b' class TextFamily(TaggedText): "A bit of text from a different family" def process(self): "Parse the type of family" self.type = self.header[1] if not self.type in TagConfig.family: Trace.error('Unrecognized family ' + type) self.output.tag = 'span' return self.output.tag = TagConfig.family[self.type] class Hfill(TaggedText): "Horizontall fill" def process(self): Trace.debug('hfill') self.output.tag = 'span class="hfill"' class BarredText(TaggedText): "Text with a bar somewhere" def process(self): "Parse the type of bar" self.type = self.header[1] if not self.type in TagConfig.barred: Trace.error('Unknown bar type ' + self.type) self.output.tag = 'span' return self.output.tag = TagConfig.barred[self.type] class LangLine(Container): "A line with language information" def __init__(self): self.parser = LoneCommand() self.output = EmptyOutput() def process(self): self.lang = self.header[1] class Space(Container): "A space of several types" def __init__(self): self.parser = InsetParser() self.output = FixedOutput() def process(self): self.type = self.header[2] if self.type not in StyleConfig.spaces: Trace.error('Unknown space type ' + self.type) self.html = [' '] return self.html = [StyleConfig.spaces[self.type]] class NumberGenerator(object): "A number generator for unique sequences and hierarchical structures" letters = '-ABCDEFGHIJKLMNOPQRSTUVWXYZ' instance = None startinglevel = 0 maxdepth = 10 unique = NumberingConfig.layouts['unique'] ordered = NumberingConfig.layouts['ordered'] def __init__(self): self.number = [] self.uniques = dict() self.chaptered = dict() def generateunique(self, type): "Generate unique numbering: a number to place in the title but not to " "append to others. Examples: Part 1, Book 3." if not type in self.uniques: self.uniques[type] = 0 self.uniques[type] = self.increase(self.uniques[type]) return unicode(self.uniques[type]) def generateordered(self, type): "Generate ordered numbering: a number to use and possibly concatenate " "with others. Example: Chapter 1, Section 1.5." level = self.getlevel(type) if level == 0: Trace.error('Impossible level 0 for ' + type) return '.' if level > NumberGenerator.maxdepth: return '' if len(self.number) >= level: self.number = self.number[:level] else: while len(self.number) < level: self.number.append(0) self.number[level - 1] = self.increase(self.number[level - 1]) return self.dotseparated(self.number) def generatechaptered(self, type): "Generate a number which goes with first-level numbers (chapters). " "For the article classes a unique number is generated." if NumberGenerator.startinglevel > 0: return self.generateunique(type) if len(self.number) == 0: chapter = 0 else: chapter = self.number[0] if not type in self.chaptered or self.chaptered[type][0] != chapter: self.chaptered[type] = [chapter, 0] chaptered = self.chaptered[type] chaptered[1] = self.increase(chaptered[1]) self.chaptered[type] = chaptered return self.dotseparated(chaptered) def getlevel(self, type): "Get the level that corresponds to a type." level = NumberGenerator.ordered.index(self.deasterisk(type)) + 1 return level - NumberGenerator.startinglevel def isunique(self, container): "Find out if a container requires unique numbering." return container.type in NumberGenerator.unique def isinordered(self, container): "Find out if a container is ordered or unordered." return self.deasterisk(container.type) in NumberGenerator.ordered def isordered(self, container): "Find out if a container requires ordered numbering." return container.type in NumberGenerator.ordered def isunordered(self, container): "Find out if a container does not have a number." if not '*' in container.type: return False return self.isinordered(container) def increase(self, number): "Increase the number (or letter)" if not isinstance(number, str): return number + 1 if not number in NumberGenerator.letters: Trace.error('Unknown letter numeration ' + number) return 0 index = NumberGenerator.letters.index(number) + 1 return NumberGenerator.letters[index % len(NumberGenerator.letters)] def dotseparated(self, number): "Get the number separated by dots: 1.1.3" dotsep = '' if len(number) == 0: Trace.error('Empty number') return '.' for piece in number: dotsep += '.' + unicode(piece) return dotsep[1:] def deasterisk(self, type): "Get the type without the asterisk for unordered types." return type.replace('*', '') NumberGenerator.instance = NumberGenerator() class LayoutNumberer(object): "Number a layout with the relevant attributes." instance = None def __init__(self): self.generator = NumberGenerator.instance def isnumbered(self, container): "Find out if a container requires numbering at all." return self.generator.deasterisk(container.type) \ in NumberGenerator.unique + NumberGenerator.ordered def number(self, layout): "Set all attributes: number, entry, level..." if self.generator.isunique(layout): layout.number = self.generator.generateunique(layout.type) layout.entry = TranslationConfig.constants[layout.type] + ' ' + layout.number layout.partkey = 'toc-' + layout.type + '-' + layout.number layout.anchortext = layout.entry + '.' layout.level = 0 return if not self.generator.isinordered(layout): Trace.error('Trying to number wrong ' + unicode(layout)) return # ordered or unordered if self.generator.isordered(layout): layout.number = self.generator.generateordered(layout.type) elif self.generator.isunordered(layout): layout.number = '' number = layout.number if number == '': # ordered but bigger than maxdepth numbered or unordered number = self.generator.generateunique(layout.type) layout.partkey = 'toc-' + layout.type + '-' + number type = self.generator.deasterisk(layout.type) layout.anchortext = layout.number layout.entry = TranslationConfig.constants[type] if layout.number != '': layout.entry += ' ' + layout.number layout.level = self.generator.getlevel(type) layout.output.tag = layout.output.tag.replace('?', unicode(layout.level)) def modifylayout(self, layout, type): "Modify a layout according to the given type." LayoutNumberer.instance = LayoutNumberer() class Link(Container): "A link to another part of the document" def __init__(self): Container.__init__(self) self.parser = InsetParser() self.output = LinkOutput() self.anchor = None self.url = None self.type = None self.page = None self.target = None self.destination = None if Options.target: self.target = Options.target def complete(self, text, anchor = None, url = None, type = None): "Complete the link." self.contents = [Constant(text)] if anchor: self.anchor = anchor if url: self.url = url if type: self.type = type return self def computedestination(self): "Use the destination link to fill in the destination URL." if not self.destination: return if not self.destination.anchor: Trace.error('Missing anchor in link destination ' + unicode(self.destination)) return self.url = '#' + self.destination.anchor if self.destination.page: self.url = self.destination.page + self.url def setmutualdestination(self, destination): "Set another link as destination, and set its destination to this one." self.destination = destination destination.destination = self class ListInset(Container): "An inset with a list, normally made of links." def __init__(self): self.parser = InsetParser() self.output = ContentsOutput() def sortdictionary(self, dictionary): "Sort all entries in the dictionary" keys = dictionary.keys() # sort by name keys.sort() return keys class ListOf(ListInset): "A list of entities (figures, tables, algorithms)" def process(self): "Parse the header and get the type" self.type = self.header[2] text = TranslationConfig.lists[self.type] self.contents = [TaggedText().constant(text, 'div class="tocheader"', True)] class TableOfContents(ListInset): "Table of contents" def process(self): "Parse the header and get the type" text = TranslationConfig.constants['toc'] self.contents = [TaggedText().constant(text, 'div class="tocheader"', True)] class IndexEntry(Link): "An entry in the alphabetical index" entries = dict() arrows = dict() namescapes = {'!':'', '|':', ', ' ':' '} keyescapes = {' ':'-', '--':'-', ',':''} def process(self): "Put entry in index" if 'name' in self.parameters: name = self.parameters['name'].strip() else: name = self.extracttext() self.name = self.escape(name, IndexEntry.namescapes) key = self.escape(self.name, IndexEntry.keyescapes) if not key in IndexEntry.entries: # no entry yet; create entry = Link().complete(name, 'index-' + key, None, 'printindex') entry.name = name IndexEntry.entries[key] = entry if not key in IndexEntry.arrows: # no arrows yet; create list IndexEntry.arrows[key] = [] self.index = len(IndexEntry.arrows[key]) self.complete(u'↓', 'entry-' + key + '-' + unicode(self.index)) self.destination = IndexEntry.entries[key] arrow = Link().complete(u'↑', 'index-' + key) arrow.destination = self IndexEntry.arrows[key].append(arrow) class PrintIndex(ListInset): "Command to print an index" def process(self): "Create the alphabetic index" index = TranslationConfig.constants['index'] self.contents = [TaggedText().constant(index, 'h1 class="index"'), Constant('\n')] for key in self.sortdictionary(IndexEntry.entries): entry = IndexEntry.entries[key] entrytext = [IndexEntry.entries[key], Constant(': ')] contents = [TaggedText().complete(entrytext, 'i')] contents += self.extractarrows(key) self.contents.append(TaggedText().complete(contents, 'p class="printindex"', True)) def extractarrows(self, key): "Extract all arrows (links to the original reference) for a key." arrows = [] for arrow in IndexEntry.arrows[key]: arrows += [arrow, Constant(u', \n')] return arrows[:-1] class NomenclatureEntry(Link): "An entry of LyX nomenclature" entries = dict() def process(self): "Put entry in index" symbol = self.parameters['symbol'] description = self.parameters['description'] key = symbol.replace(' ', '-').lower() if key in NomenclatureEntry.entries: Trace.error('Duplicated nomenclature entry ' + key) self.complete(u'↓', 'noment-' + key) entry = Link().complete(u'↑', 'nom-' + key) entry.symbol = symbol entry.description = description self.setmutualdestination(entry) NomenclatureEntry.entries[key] = entry class PrintNomenclature(ListInset): "Print all nomenclature entries" def process(self): nomenclature = TranslationConfig.constants['nomenclature'] self.contents = [TaggedText().constant(nomenclature, 'h1 class="nomenclature"')] for key in self.sortdictionary(NomenclatureEntry.entries): entry = NomenclatureEntry.entries[key] contents = [entry, Constant(entry.symbol + u' ' + entry.description)] text = TaggedText().complete(contents, 'div class="Nomenclated"', True) self.contents.append(text) class URL(Link): "A clickable URL" def process(self): "Read URL from parameters" name = self.escape(self.parameters['target']) if 'type' in self.parameters: self.url = self.escape(self.parameters['type']) + name else: self.url = name if 'name' in self.parameters: name = self.parameters['name'] self.contents = [Constant(name)] class FlexURL(URL): "A flexible URL" def process(self): "Read URL from contents" self.url = self.extracttext() class LinkOutput(object): "A link pointing to some destination" "Or an anchor (destination)" def gethtml(self, link): "Get the HTML code for the link" type = link.__class__.__name__ if link.type: type = link.type tag = 'a class="' + type + '"' if link.anchor: tag += ' name="' + link.anchor + '"' if link.destination: link.computedestination() if link.url: tag += ' href="' + link.url + '"' if link.target: tag += ' target="' + link.target + '"' text = TaggedText().complete(link.contents, tag) return text.gethtml() class Label(Link): "A label to be referenced" names = dict() def process(self): "Process a label container." key = self.parameters['name'] self.create(' ', key) def create(self, text, key, type = 'Label'): "Create the label for a given key." self.key = key self.complete(text, anchor = key, type = type) Label.names[key] = self if key in Reference.references: for reference in Reference.references[key]: reference.destination = self return self def __unicode__(self): "Return a printable representation." return 'Label ' + self.key class Reference(Link): "A reference to a label" references = dict() def process(self): "Read the reference and set the arrow." self.key = self.parameters['reference'] if self.key in Label.names: direction = u'↑' label = Label.names[self.key] else: direction = u'↓' label = Label().complete(' ', self.key, 'preref') self.destination = label self.contents = [Constant(direction)] if not self.key in Reference.references: Reference.references[self.key] = [] Reference.references[self.key].append(self) def __unicode__(self): "Return a printable representation." return 'Reference ' + self.key class LyXHeader(Container): "Reads the header, outputs the HTML header" indentstandard = False tocdepth = 10 def __init__(self): self.contents = [] self.parser = HeaderParser() self.output = HeaderOutput() def process(self): "Find pdf title" TitleOutput.pdftitle = self.getparameter('pdftitle') if self.getparameter('documentclass') in HeaderConfig.styles['article']: NumberGenerator.startinglevel = 1 if self.getparameter('paragraphseparation') == 'indent': LyXHeader.indentstandard = True LyXHeader.tocdepth = self.getlevel('tocdepth') NumberGenerator.maxdepth = self.getlevel('secnumdepth') def getparameter(self, configparam): "Get a parameter configured in HeaderConfig." key = HeaderConfig.parameters[configparam] if not key in self.parameters: return None return self.parameters[key] def getlevel(self, configparam): "Get a level read as a parameter from HeaderConfig." value = int(self.getparameter(configparam)) if NumberGenerator.startinglevel == 1: return value return value + 1 class LyXFooter(Container): "Reads the footer, outputs the HTML footer" def __init__(self): self.contents = [] self.parser = BoundedDummy() self.output = FooterOutput() class Align(Container): "Bit of aligned text" def __init__(self): self.parser = ExcludingParser() self.output = TaggedOutput().setbreaklines(True) def process(self): self.output.tag = 'div class="' + self.header[1] + '"' class Newline(Container): "A newline" def __init__(self): self.parser = LoneCommand() self.output = FixedOutput() def process(self): "Process contents" self.html = ['
\n'] class NewPage(Newline): "A new page" def process(self): "Process contents" self.html = ['


\n

\n'] class Appendix(Container): "An appendix to the main document" def __init__(self): self.parser = LoneCommand() self.output = EmptyOutput() class ListItem(Container): "An element in a list" def __init__(self): "Output should be empty until the postprocessor can group items" self.contents = list() self.parser = BoundedParser() self.output = EmptyOutput() def process(self): "Set the correct type and contents." self.type = self.header[1] tag = TaggedText().complete(self.contents, 'li', True) self.contents = [tag] def __unicode__(self): return self.type + ' item @ ' + unicode(self.begin) class DeeperList(Container): "A nested list" def __init__(self): "Output should be empty until the postprocessor can group items" self.parser = BoundedParser() self.output = EmptyOutput() def process(self): "Create the deeper list" if len(self.contents) == 0: Trace.error('Empty deeper list') return def __unicode__(self): result = 'deeper list @ ' + unicode(self.begin) + ': [' for element in self.contents: result += unicode(element) + ', ' return result[:-2] + ']' class ERT(Container): "Evil Red Text" def __init__(self): self.parser = InsetParser() self.output = EmptyOutput() class Layout(Container): "A layout (block of text) inside a lyx file" def __init__(self): self.contents = list() self.parser = BoundedParser() self.output = TaggedOutput().setbreaklines(True) def process(self): self.type = self.header[1] if self.type in TagConfig.layouts: self.output.tag = TagConfig.layouts[self.type] + ' class="' + self.type + '"' elif self.type.replace('*', '') in TagConfig.layouts: self.output.tag = TagConfig.layouts[self.type.replace('*', '')] + ' class="' + self.type.replace('*', '-') + '"' else: self.output.tag = 'div class="' + self.type + '"' def __unicode__(self): return 'Layout of type ' + self.type class StandardLayout(Layout): "A standard layout -- can be a true div or nothing at all" indentation = False def process(self): self.type = 'standard' self.output = ContentsOutput() def complete(self, contents): "Set the contents and return it." self.process() self.contents = contents return self class Title(Layout): "The title of the whole document" def process(self): self.type = 'title' self.output.tag = 'h1 class="title"' self.title = self.extracttext() TitleOutput.title = self.title Trace.message('Title: ' + self.title) class Author(Layout): "The document author" def process(self): self.type = 'author' self.output.tag = 'h2 class="author"' strings = self.searchall(StringContainer) if len(strings) > 0: FooterOutput.author = strings[0].string Trace.debug('Author: ' + FooterOutput.author) class Abstract(Layout): "A paper abstract" def process(self): self.type = 'abstract' self.output.tag = 'div class="abstract"' message = TranslationConfig.constants['abstract'] tagged = TaggedText().constant(message, 'p class="abstract-message"', True) self.contents.insert(0, tagged) class FirstWorder(Layout): "A layout where the first word is extracted" def extractfirstword(self, contents): "Extract the first word as a list" first, found = self.extractfirsttuple(contents) return first def extractfirsttuple(self, contents): "Extract the first word as a tuple" firstcontents = [] index = 0 while index < len(contents): first, found = self.extractfirstcontainer(contents[index]) if first: firstcontents += first if found: return firstcontents, True else: del contents[index] return firstcontents, False def extractfirstcontainer(self, container): "Extract the first word from a string container" if isinstance(container, StringContainer): return self.extractfirststring(container) if isinstance(container, ERT): return [container], False if len(container.contents) == 0: # empty container return [container], False first, found = self.extractfirsttuple(container.contents) if isinstance(container, TaggedText) and hasattr(container, 'tag'): newtag = TaggedText().complete(first, container.tag) return [newtag], found return first, found def extractfirststring(self, container): "Extract the first word from a string container" string = container.string if not ' ' in string: return [container], False split = string.split(' ', 1) container.string = split[1] return [Constant(split[0])], True class Description(FirstWorder): "A description layout" def process(self): "Set the first word to bold" self.type = 'Description' self.output.tag = 'div class="Description"' firstword = self.extractfirstword(self.contents) if not firstword: return firstword.append(Constant(u' ')) tag = 'span class="Description-entry"' self.contents.insert(0, TaggedText().complete(firstword, tag)) class List(FirstWorder): "A list layout" def process(self): "Set the first word to bold" self.type = 'List' self.output.tag = 'div class="List"' firstword = self.extractfirstword(self.contents) if not firstword: return tag = 'span class="List-entry"' self.contents.insert(0, TaggedText().complete(firstword, tag)) class PlainLayout(Layout): "A plain layout" def process(self): "Output just as contents." self.output = ContentsOutput() self.type = 'Plain' class InsetText(Container): "An inset of text in a lyx file" def __init__(self): self.parser = BoundedParser() self.output = ContentsOutput() class Inset(Container): "A generic inset in a LyX document" def __init__(self): self.contents = list() self.parser = InsetParser() self.output = TaggedOutput().setbreaklines(True) def process(self): self.type = self.header[1] self.output.tag = 'span class="' + self.type + '"' def __unicode__(self): return 'Inset of type ' + self.type class NewlineInset(Newline): "A newline or line break in an inset" def __init__(self): self.parser = InsetParser() self.output = FixedOutput() class Branch(Container): "A branch within a LyX document" def __init__(self): self.parser = InsetParser() self.output = TaggedOutput().settag('span class="branch"', True) def process(self): "Disable inactive branches" self.branch = self.header[2] if not self.isactive(): Trace.debug('Branch ' + self.branch + ' not active') self.output = EmptyOutput() def isactive(self): "Check if the branch is active" if not self.branch in Options.branches: Trace.error('Invalid branch ' + self.branch) return True branch = Options.branches[self.branch] return branch.isselected() class ShortTitle(Container): "A short title to display (always hidden)" def __init__(self): self.parser = InsetParser() self.output = EmptyOutput() class Footnote(Container): "A footnote to the main text" order = 0 list = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' def __init__(self): self.parser = InsetParser() self.output = ContentsOutput() def process(self): "Add a letter for the order, rotating" letter = Footnote.list[Footnote.order % len(Footnote.list)] span = 'span class="FootMarker"' pre = FootnoteConfig.constants['prefrom'] post = FootnoteConfig.constants['postfrom'] fromfoot = TaggedText().constant(pre + letter + post, span) self.contents.insert(0, fromfoot) tag = TaggedText().complete(self.contents, 'span class="Foot"', True) pre = FootnoteConfig.constants['preto'] post = FootnoteConfig.constants['postto'] tofoot = TaggedText().constant(pre + letter + post, span) self.contents = [tofoot, tag] Footnote.order += 1 class Note(Container): "A LyX note of several types" def __init__(self): self.parser = InsetParser() self.output = EmptyOutput() def process(self): "Hide note and comment, dim greyed out" self.type = self.header[2] if TagConfig.notes[self.type] == '': return self.output = TaggedOutput().settag(TagConfig.notes[self.type], True) class FlexCode(Container): "A bit of inset code" def __init__(self): self.parser = InsetParser() self.output = TaggedOutput().settag('span class="code"', True) class InfoInset(Container): "A LyX Info inset" def __init__(self): self.parser = InsetParser() self.output = TaggedOutput().settag('span class="Info"', False) def process(self): "Set the shortcut as text" self.type = self.parameters['type'] self.contents = [Constant(self.parameters['arg'])] class BoxInset(Container): "A box inset" def __init__(self): self.parser = InsetParser() self.output = TaggedOutput().settag('div', True) def process(self): "Set the correct tag" self.type = self.header[2] if not self.type in TagConfig.boxes: Trace.error('Uknown box type ' + self.type) return self.output.settag(TagConfig.boxes[self.type], True) class IncludeInset(Container): "A child document included within another." # the converter factory will be set in converter.py converterfactory = None def __init__(self): self.parser = InsetParser() self.output = ContentsOutput() def process(self): "Include the provided child document" self.filename = self.parameters['filename'] Trace.debug('Child document: ' + self.filename) if 'lstparams' in self.parameters: self.parselstparams() converter = IncludeInset.converterfactory.create(self) converter.convert() self.contents = converter.getcontents() class PostLayout(object): "Numerate an indexed layout" processedclass = Layout def postprocess(self, last, layout, next): "Generate a number and place it before the text" if not LayoutNumberer.instance.isnumbered(layout): return layout if self.containsappendix(layout): self.activateappendix() LayoutNumberer.instance.number(layout) label = Label().create(layout.anchortext, layout.partkey, type='toc') layout.contents.insert(0, label) if layout.number != '': layout.contents.insert(1, Constant(u' ')) return layout def modifylayout(self, layout, type): "Modify a layout according to the given type." layout.level = NumberGenerator.instance.getlevel(type) layout.output.tag = layout.output.tag.replace('?', unicode(layout.level)) def containsappendix(self, layout): "Find out if there is an appendix somewhere in the layout" for element in layout.contents: if isinstance(element, Appendix): return True return False def activateappendix(self): "Change first number to letter, and chapter to appendix" NumberGenerator.instance.number = ['-'] class PostStandard(object): "Convert any standard spans in root to divs" processedclass = StandardLayout def postprocess(self, last, standard, next): "Switch to div" type = 'Standard' if LyXHeader.indentstandard: if isinstance(last, StandardLayout): type = 'Indented' else: type = 'Unindented' standard.output = TaggedOutput().settag('div class="' + type + '"', True) return standard class Postprocessor(object): "Postprocess a container keeping some context" stages = [PostLayout, PostStandard] def __init__(self): self.stages = StageDict(Postprocessor.stages, self) self.current = None self.last = None def postprocess(self, next): "Postprocess the root container and its contents" self.postrecursive(self.current) result = self.postcurrent(next) self.last = self.current self.current = next return result def postrecursive(self, container): "Postprocess the container contents recursively" if not hasattr(container, 'contents'): return if len(container.contents) == 0: return postprocessor = Postprocessor() contents = [] for element in container.contents: post = postprocessor.postprocess(element) if post: contents.append(post) # two rounds to empty the pipeline for i in range(2): post = postprocessor.postprocess(None) if post: contents.append(post) container.contents = contents def postcurrent(self, next): "Postprocess the current element taking into account next and last." stage = self.stages.getstage(self.current) if not stage: return self.current return stage.postprocess(self.last, self.current, next) class StageDict(object): "A dictionary of stages corresponding to classes" def __init__(self, classes, postprocessor): "Instantiate an element from each class and store as a dictionary" instances = self.instantiate(classes, postprocessor) self.stagedict = dict([(x.processedclass, x) for x in instances]) def instantiate(self, classes, postprocessor): "Instantiate an element from each class" stages = [x.__new__(x) for x in classes] for element in stages: element.__init__() element.postprocessor = postprocessor return stages def getstage(self, element): "Get the stage for a given element, if the type is in the dict" if not element.__class__ in self.stagedict: return None return self.stagedict[element.__class__] class BiblioCite(Container): "Cite of a bibliography entry" cites = dict() generator = NumberGenerator() def __init__(self): self.parser = InsetParser() self.output = TaggedOutput().settag('sup') self.contents = [] self.entries = [] def process(self): "Add a cite to every entry" keys = self.parameters['key'].split(',') for key in keys: number = NumberGenerator.instance.generateunique('bibliocite') entry = self.createentry(key, number) cite = Link().complete(number, 'cite-' + number, type='bibliocite') cite.setmutualdestination(entry) self.contents += [cite, Constant(',')] if not key in BiblioCite.cites: BiblioCite.cites[key] = [] BiblioCite.cites[key].append(cite) if len(keys) > 0: # remove trailing , self.contents.pop() def createentry(self, key, number): "Create the entry with the given key and number." entry = Link().complete(number, 'biblio-' + number, type='biblioentry') if not key in BiblioEntry.entries: BiblioEntry.entries[key] = [] BiblioEntry.entries[key].append(entry) return entry class Bibliography(Container): "A bibliography layout containing an entry" def __init__(self): self.parser = BoundedParser() self.output = TaggedOutput().settag('p class="biblio"', True) class BiblioEntry(Container): "A bibliography entry" entries = dict() def __init__(self): self.parser = InsetParser() self.output = TaggedOutput().settag('span class="entry"') def process(self): "Process the cites for the entry's key" self.processcites(self.parameters['key']) def processcites(self, key): "Get all the cites of the entry" self.key = key if not key in BiblioEntry.entries: self.contents.append(Constant('[-] ')) return entries = BiblioEntry.entries[key] self.contents = [Constant('[')] for entry in entries: self.contents.append(entry) self.contents.append(Constant(',')) self.contents.pop(-1) self.contents.append(Constant('] ')) class PostBiblio(object): "Insert a Bibliography legend before the first item" processedclass = Bibliography def postprocess(self, last, element, next): "If we have the first bibliography insert a tag" if isinstance(last, Bibliography): return element bibliography = TranslationConfig.constants['bibliography'] header = TaggedText().constant(bibliography, 'h1 class="biblio"') layout = StandardLayout().complete([header, element]) return layout Postprocessor.stages.append(PostBiblio) import sys class Cloner(object): "An object used to clone other objects." def clone(cls, original): "Return an exact copy of an object." "The original object must have an empty constructor." type = original.__class__ clone = type.__new__(type) clone.__init__() return clone clone = classmethod(clone) import sys import sys class Position(object): "A position in a text to parse" def __init__(self, text): self.text = text self.pos = 0 self.endinglist = EndingList() def skip(self, string): "Skip a string" self.pos += len(string) def remaining(self): "Return the text remaining for parsing" return self.text[self.pos:] def finished(self): "Find out if the current formula has finished" if self.isout(): self.endinglist.checkpending() return True return self.endinglist.checkin(self) def isout(self): "Find out if we are out of the formula yet" return self.pos >= len(self.text) def current(self): "Return the current character" if self.isout(): Trace.error('Out of the formula') return '' return self.text[self.pos] def currentskip(self): "Return the current character and skip it." current = self.current() self.skip(current) return current def checkfor(self, string): "Check for a string at the given position" if self.pos + len(string) > len(self.text): return False return self.text[self.pos : self.pos + len(string)] == string def checkskip(self, string): "Check for a string at the given position; if there, skip it" if not self.checkfor(string): return False self.skip(string) return True def glob(self, currentcheck): "Glob a bit of text that satisfies a check" glob = '' while not self.finished() and currentcheck(self.current()): glob += self.current() self.skip(self.current()) return glob def globalpha(self): "Glob a bit of alpha text" return self.glob(lambda current: current.isalpha()) def skipspace(self): "Skip all whitespace at current position" return self.glob(lambda current: current.isspace()) def globincluding(self, magicchar): "Glob a bit of text up to (including) the magic char." glob = self.glob(lambda current: current != magicchar) + magicchar self.skip(magicchar) return glob def globexcluding(self, magicchar): "Glob a bit of text up until (excluding) the magic char." return self.glob(lambda current: current != magicchar) def pushending(self, ending, optional = False): "Push a new ending to the bottom" self.endinglist.add(ending, optional) def popending(self, expected = None): "Pop the ending found at the current position" ending = self.endinglist.pop(self) if expected and expected != ending: Trace.error('Expected ending ' + expected + ', got ' + ending) self.skip(ending) return ending class EndingList(object): "A list of position endings" def __init__(self): self.endings = [] def add(self, ending, optional): "Add a new ending to the list" self.endings.append(PositionEnding(ending, optional)) def checkin(self, pos): "Search for an ending" if self.findending(pos): return True return False def pop(self, pos): "Remove the ending at the current position" ending = self.findending(pos) if not ending: Trace.error('No ending at ' + pos.current()) return '' for each in reversed(self.endings): self.endings.remove(each) if each == ending: return each.ending elif not each.optional: Trace.error('Removed non-optional ending ' + each) Trace.error('No endings left') return '' def findending(self, pos): "Find the ending at the current position" if len(self.endings) == 0: return None for index, ending in enumerate(reversed(self.endings)): if ending.checkin(pos): return ending if not ending.optional: return None return None def checkpending(self): "Check if there are any pending endings" if len(self.endings) != 0: Trace.error('Pending ' + unicode(self) + ' left open') def __unicode__(self): "Printable representation" string = 'endings [' for ending in self.endings: string += unicode(ending) + ',' if len(self.endings) > 0: string = string[:-1] return string + ']' class PositionEnding(object): "An ending for a parsing position" def __init__(self, ending, optional): self.ending = ending self.optional = optional def checkin(self, pos): "Check for the ending" return pos.checkfor(self.ending) def __unicode__(self): "Printable representation" string = 'Ending ' + self.ending if self.optional: string += ' (optional)' return string class FormulaParser(Parser): "Parses a formula" def parseheader(self, reader): "See if the formula is inlined" self.begin = reader.linenumber + 1 if reader.currentline().find(FormulaConfig.starts['simple']) > 0: return ['inline'] if reader.currentline().find(FormulaConfig.starts['complex']) > 0: return ['block'] if reader.currentline().find(FormulaConfig.starts['unnumbered']) > 0: return ['block'] return ['numbered'] def parse(self, reader): "Parse the formula until the end" formula = self.parseformula(reader) while not reader.currentline().startswith(self.ending): stripped = reader.currentline().strip() if len(stripped) > 0: Trace.error('Unparsed formula line ' + stripped) reader.nextline() reader.nextline() return [formula] def parseformula(self, reader): "Parse the formula contents" simple = FormulaConfig.starts['simple'] if simple in reader.currentline(): rest = reader.currentline().split(simple, 1)[1] if simple in rest: # formula is $...$ return self.parsesingleliner(reader, simple, simple) # formula is multiline $...$ return self.parsemultiliner(reader, simple, simple) if FormulaConfig.starts['complex'] in reader.currentline(): # formula of the form \[...\] return self.parsemultiliner(reader, FormulaConfig.starts['complex'], FormulaConfig.endings['complex']) beginbefore = FormulaConfig.starts['beginbefore'] beginafter = FormulaConfig.starts['beginafter'] if beginbefore in reader.currentline(): if reader.currentline().strip().endswith(beginafter): current = reader.currentline().strip() endsplit = current.split(beginbefore)[1].split(beginafter) startpiece = beginbefore + endsplit[0] + beginafter endbefore = FormulaConfig.endings['endbefore'] endafter = FormulaConfig.endings['endafter'] endpiece = endbefore + endsplit[0] + endafter return startpiece + self.parsemultiliner(reader, startpiece, endpiece) + endpiece Trace.error('Missing ' + beginafter + ' in ' + reader.currentline()) return '' begincommand = FormulaConfig.starts['command'] beginbracket = FormulaConfig.starts['bracket'] if begincommand in reader.currentline() and beginbracket in reader.currentline(): endbracket = FormulaConfig.endings['bracket'] return self.parsemultiliner(reader, beginbracket, endbracket) Trace.error('Formula beginning ' + reader.currentline() + ' is unknown') return '' def parsesingleliner(self, reader, start, ending): "Parse a formula in one line" line = reader.currentline().strip() if not start in line: Trace.error('Line ' + line + ' does not contain formula start ' + start) return '' if not line.endswith(ending): Trace.error('Formula ' + line + ' does not end with ' + ending) return '' index = line.index(start) rest = line[index + len(start):-len(ending)] reader.nextline() return rest def parsemultiliner(self, reader, start, ending): "Parse a formula in multiple lines" formula = '' line = reader.currentline() if not start in line: Trace.error('Line ' + line.strip() + ' does not contain formula start ' + start) return '' index = line.index(start) line = line[index + len(start):].strip() while not line.endswith(ending): formula += line reader.nextline() line = reader.currentline() formula += line[:-len(ending)] reader.nextline() return formula class Formula(Container): "A LaTeX formula" def __init__(self): self.parser = FormulaParser() self.output = TaggedOutput().settag('span class="formula"') def process(self): "Convert the formula to tags" pos = Position(self.contents[0]) whole = WholeFormula() if not whole.detect(pos): Trace.error('Unknown formula at: ' + pos.remaining()) constant = TaggedBit().constant(pos.remaining(), 'span class="unknown"') self.contents = [constant] return whole.parsebit(pos) whole.process() self.contents = [whole] whole.parent = self if self.header[0] != 'inline': self.output.settag('div class="formula"', True) class FormulaBit(Container): "A bit of a formula" def __init__(self): # type can be 'alpha', 'number', 'font' self.type = None self.original = '' self.contents = [] self.output = ContentsOutput() def add(self, bit): "Add any kind of formula bit already processed" self.contents.append(bit) self.original += bit.original bit.parent = self def skiporiginal(self, string, pos): "Skip a string and add it to the original formula" self.original += string if not pos.checkskip(string): Trace.error('String ' + string + ' not at ' + pos.remaining()) def __unicode__(self): "Get a string representation" return self.__class__.__name__ + ' read in ' + self.original class TaggedBit(FormulaBit): "A tagged string in a formula" def constant(self, constant, tag): "Set the constant and the tag" self.output = TaggedOutput().settag(tag) self.add(FormulaConstant(constant)) return self def complete(self, contents, tag): "Set the constant and the tag" self.contents = contents self.output = TaggedOutput().settag(tag) return self class FormulaConstant(FormulaBit): "A constant string in a formula" def __init__(self, string): "Set the constant string" FormulaBit.__init__(self) self.original = string self.output = FixedOutput() self.html = [string] class WholeFormula(FormulaBit): "Parse a whole formula" def __init__(self): FormulaBit.__init__(self) self.factory = FormulaFactory() def detect(self, pos): "Check in the factory" return self.factory.detectbit(pos) def parsebit(self, pos): "Parse with any formula bit" while self.factory.detectbit(pos): bit = self.factory.parsebit(pos) #Trace.debug(bit.original + ' -> ' + unicode(bit.gethtml())) self.add(bit) def process(self): "Process the whole formula" for index, bit in enumerate(self.contents): bit.process() # no units processing continue if bit.type == 'alpha': # make variable self.contents[index] = TaggedBit().complete([bit], 'i') elif bit.type == 'font' and index > 0: last = self.contents[index - 1] if last.type == 'number': #separate last.contents.append(FormulaConstant(u' ')) class FormulaFactory(object): "Construct bits of formula" # bits will be appended later bits = [] def detectbit(self, pos): "Detect if there is a next bit" if pos.finished(): return False for bit in FormulaFactory.bits: if bit.detect(pos): return True return False def parsebit(self, pos): "Parse just one formula bit." for bit in FormulaFactory.bits: if bit.detect(pos): # get a fresh bit and parse it newbit = Cloner.clone(bit) newbit.factory = self returnedbit = newbit.parsebit(pos) if returnedbit: return returnedbit return newbit Trace.error('Unrecognized formula at ' + pos.remaining()) return FormulaConstant(pos.currentskip()) import sys import sys class RawText(FormulaBit): "A bit of text inside a formula" def detect(self, pos): "Detect a bit of raw text" return pos.current().isalpha() def parsebit(self, pos): "Parse alphabetic text" alpha = pos.globalpha() self.add(FormulaConstant(alpha)) self.type = 'alpha' class FormulaSymbol(FormulaBit): "A symbol inside a formula" modified = FormulaConfig.modified unmodified = FormulaConfig.unmodified['characters'] def detect(self, pos): "Detect a symbol" if pos.current() in FormulaSymbol.unmodified: return True if pos.current() in FormulaSymbol.modified: return True return False def parsebit(self, pos): "Parse the symbol" if pos.current() in FormulaSymbol.unmodified: self.addsymbol(pos.current(), pos) return if pos.current() in FormulaSymbol.modified: self.addsymbol(FormulaSymbol.modified[pos.current()], pos) return Trace.error('Symbol ' + pos.current() + ' not found') def addsymbol(self, symbol, pos): "Add a symbol" self.skiporiginal(pos.current(), pos) self.contents.append(FormulaConstant(symbol)) class Number(FormulaBit): "A string of digits in a formula" def detect(self, pos): "Detect a digit" return pos.current().isdigit() def parsebit(self, pos): "Parse a bunch of digits" digits = pos.glob(lambda current: current.isdigit()) self.add(FormulaConstant(digits)) self.type = 'number' class Bracket(FormulaBit): "A {} bracket inside a formula" start = FormulaConfig.starts['bracket'] ending = FormulaConfig.endings['bracket'] def __init__(self): "Create a (possibly literal) new bracket" FormulaBit.__init__(self) self.inner = None def detect(self, pos): "Detect the start of a bracket" return pos.checkfor(self.start) def parsebit(self, pos): "Parse the bracket" self.parsecomplete(pos, self.innerformula) return self def parsetext(self, pos): "Parse a text bracket" self.parsecomplete(pos, self.innertext) return self def parseliteral(self, pos): "Parse a literal bracket" self.parsecomplete(pos, self.innerliteral) return self def parsecomplete(self, pos, innerparser): "Parse the start and end marks" if not pos.checkfor(self.start): Trace.error('Bracket should start with ' + self.start + ' at ' + pos.remaining()) return self.skiporiginal(self.start, pos) pos.pushending(self.ending) innerparser(pos) self.original += pos.popending(self.ending) def innerformula(self, pos): "Parse a whole formula inside the bracket" self.inner = WholeFormula() if self.inner.detect(pos): self.inner.parsebit(pos) self.add(self.inner) return if pos.finished(): return if pos.current() != self.ending: Trace.error('No formula in bracket at ' + pos.remaining()) return def innertext(self, pos): "Parse some text inside the bracket, following textual rules." factory = FormulaFactory() while not pos.finished(): if pos.current() == FormulaConfig.starts['command']: bit = factory.parsebit(pos) pos.checkskip(' ') else: bit = FormulaConstant(pos.currentskip()) self.add(bit) def innerliteral(self, pos): "Parse a literal inside the bracket, which cannot generate html" self.literal = pos.globexcluding(self.ending) self.original += self.literal def process(self): "Process the bracket" if self.inner: self.inner.process() class SquareBracket(Bracket): "A [] bracket inside a formula" start = FormulaConfig.starts['squarebracket'] ending = FormulaConfig.endings['squarebracket'] FormulaFactory.bits += [ FormulaSymbol(), RawText(), Number(), Bracket() ] class FormulaCommand(FormulaBit): "A LaTeX command inside a formula" commandbits = [] def detect(self, pos): "Find the current command" return pos.checkfor(FormulaConfig.starts['command']) def parsebit(self, pos): "Parse the command" command = self.extractcommand(pos) for bit in FormulaCommand.commandbits: if bit.recognize(command): newbit = Cloner.clone(bit) newbit.factory = self.factory newbit.setcommand(command) newbit.parsebit(pos) self.add(newbit) return newbit Trace.error('Unknown command ' + command) self.output = TaggedOutput().settag('span class="unknown"') self.add(FormulaConstant(command)) def extractcommand(self, pos): "Extract the command from the current position" start = FormulaConfig.starts['command'] if not pos.checkskip(start): Trace.error('Missing command start ' + start) return if pos.current().isalpha(): # alpha command return start + pos.globalpha() # symbol command return start + pos.currentskip() def process(self): "Process the internals" for bit in self.contents: bit.process() class CommandBit(FormulaCommand): "A formula bit that includes a command" def recognize(self, command): "Recognize the command as own" return command in self.commandmap def setcommand(self, command): "Set the command in the bit" self.command = command self.original += command self.translated = self.commandmap[self.command] def parseparameter(self, pos): "Parse a parameter at the current position" if not self.factory.detectbit(pos): Trace.error('No parameter found at: ' + pos.remaining()) return parameter = self.factory.parsebit(pos) self.add(parameter) return parameter def parsesquare(self, pos): "Parse a square bracket" bracket = SquareBracket() if not bracket.detect(pos): return None bracket.parsebit(pos) self.add(bracket) return bracket class EmptyCommand(CommandBit): "An empty command (without parameters)" commandmap = FormulaConfig.commands def parsebit(self, pos): "Parse a command without parameters" self.contents = [FormulaConstant(self.translated)] class AlphaCommand(EmptyCommand): "A command without paramters whose result is alphabetical" commandmap = FormulaConfig.alphacommands def parsebit(self, pos): "Parse the command and set type to alpha" EmptyCommand.parsebit(self, pos) self.type = 'alpha' class OneParamFunction(CommandBit): "A function of one parameter" commandmap = FormulaConfig.onefunctions def parsebit(self, pos): "Parse a function with one parameter" self.output = TaggedOutput().settag(self.translated) self.parseparameter(pos) self.simplifyifpossible() def simplifyifpossible(self): "Try to simplify to a single character." Trace.debug('Original: ' + self.original) if self.original in self.commandmap: self.output = FixedOutput() self.html = [self.commandmap[self.original]] Trace.debug('Simplified: ' + self.commandmap[self.original]) class SymbolFunction(CommandBit): "Find a function which is represented by a symbol (like _ or ^)" commandmap = FormulaConfig.symbolfunctions def detect(self, pos): "Find the symbol" return pos.current() in SymbolFunction.commandmap def parsebit(self, pos): "Parse the symbol" self.setcommand(pos.current()) pos.skip(self.command) self.output = TaggedOutput().settag(self.translated) self.parseparameter(pos) class TextFunction(CommandBit): "A function where parameters are read as text." commandmap = FormulaConfig.textfunctions def parsebit(self, pos): "Parse a text parameter" self.output = TaggedOutput().settag(self.translated) bracket = Bracket().parsetext(pos) self.add(bracket) def process(self): "Set the type to font" self.type = 'font' class LabelFunction(CommandBit): "A function that acts as a label" commandmap = FormulaConfig.labelfunctions def parsebit(self, pos): "Parse a literal parameter" self.key = Bracket().parseliteral(pos).literal def process(self): "Add an anchor with the label contents." self.type = 'font' self.label = Label().create(' ', self.key, type = 'eqnumber') self.contents = [self.label] # store as a Label so we know it's been seen Label.names[self.key] = self.label class FontFunction(OneParamFunction): "A function of one parameter that changes the font" commandmap = FormulaConfig.fontfunctions def process(self): "Simplify if possible using a single character." self.type = 'font' self.simplifyifpossible() class DecoratingFunction(OneParamFunction): "A function that decorates some bit of text" commandmap = FormulaConfig.decoratingfunctions def parsebit(self, pos): "Parse a decorating function" self.output = TaggedOutput().settag('span class="withsymbol"') self.type = 'alpha' symbol = self.translated tagged = TaggedBit().constant(symbol, 'span class="symbolover"') self.contents.append(tagged) parameter = self.parseparameter(pos) parameter.output = TaggedOutput().settag('span class="undersymbol"') self.simplifyifpossible() FormulaFactory.bits += [FormulaCommand(), SymbolFunction()] FormulaCommand.commandbits = [ EmptyCommand(), AlphaCommand(), OneParamFunction(), DecoratingFunction(), FontFunction(), LabelFunction(), TextFunction(), ] import sys class HybridFunction(CommandBit): "Read a function with two parameters: [] and {}" "The [] parameter is optional" commandmap = FormulaConfig.hybridfunctions parambrackets = [('[', ']'), ('{', '}')] def parsebit(self, pos): "Parse a function with [] and {} parameters" readtemplate = self.translated[0] writetemplate = self.translated[1] params = self.readparams(readtemplate, pos) self.contents = self.writeparams(params, writetemplate) def readparams(self, readtemplate, pos): "Read the params according to the template." params = dict() for paramdef in self.paramdefs(readtemplate): if paramdef.startswith('['): value = self.parsesquare(pos) elif paramdef.startswith('{'): value = self.parseparameter(pos) else: Trace.error('Invalid parameter definition ' + paramdef) value = None params[paramdef[1:-1]] = value return params def paramdefs(self, readtemplate): "Read each param definition in the template" pos = Position(readtemplate) while not pos.finished(): paramdef = self.readparamdef(pos) if paramdef: if len(paramdef) != 4: Trace.error('Parameter definition ' + paramdef + ' has wrong length') else: yield paramdef def readparamdef(self, pos): "Read a single parameter definition: [$0], {$x}..." for (opening, closing) in HybridFunction.parambrackets: if pos.checkskip(opening): if not pos.checkfor('$'): Trace.error('Wrong parameter name ' + pos.current()) return None return opening + pos.globincluding(closing) Trace.error('Wrong character in parameter template' + pos.currentskip()) return None def writeparams(self, params, writetemplate): "Write all params according to the template" return self.writepos(params, Position(writetemplate)) def writepos(self, params, pos): "Write all params as read in the parse position." result = [] while not pos.finished(): if pos.checkskip('$'): param = self.writeparam(params, pos) if param: result.append(param) elif pos.checkskip('f'): function = self.writefunction(params, pos) if function: result.append(function) else: result.append(FormulaConstant(pos.currentskip())) return result def writeparam(self, params, pos): "Write a single param of the form $0, $x..." name = '$' + pos.currentskip() if not name in params: Trace.error('Unknown parameter ' + name) return None if not params[name]: return None if pos.checkskip('.'): params[name].type = pos.globalpha() return params[name] def writefunction(self, params, pos): "Write a single function f0,...,fn." tag = self.readtag(params, pos) if not tag: return None if not pos.checkskip('{'): Trace.error('Function should be defined in {}') return None pos.pushending('}') contents = self.writepos(params, pos) pos.popending() if len(contents) == 0: return None function = TaggedBit().complete(contents, tag) function.type = None return function def readtag(self, params, pos): "Get the tag corresponding to the given index. Does parameter substitution." if not pos.current().isdigit(): Trace.error('Function should be f0,...,f9: f' + pos.current()) return None index = int(pos.currentskip()) if 2 + index > len(self.translated): Trace.error('Function f' + unicode(index) + ' is not defined') return None tag = self.translated[2 + index] if not '$' in tag: return tag for name in params: if name in tag: if params[name]: value = params[name].original[1:-1] else: value = '' tag = tag.replace(name, value) return tag FormulaCommand.commandbits += [ HybridFunction(), ] class TableParser(BoundedParser): "Parse the whole table" headers = ContainerConfig.table['headers'] def __init__(self): BoundedParser.__init__(self) self.columns = list() def parseheader(self, reader): "Parse table headers" reader.nextline() while self.startswithheader(reader): self.parseparameter(reader) return [] def startswithheader(self, reader): "Check if the current line starts with a header line" for start in TableParser.headers: if reader.currentline().strip().startswith(start): return True return False class TablePartParser(BoundedParser): "Parse a table part (row or cell)" def parseheader(self, reader): "Parse the header" tablekey, parameters = self.parsexml(reader) self.parameters = parameters return list() class ColumnParser(LoneCommand): "Parse column properties" def parseheader(self, reader): "Parse the column definition" key, parameters = self.parsexml(reader) self.parameters = parameters return [] class Table(Container): "A lyx table" def __init__(self): self.parser = TableParser() self.output = TaggedOutput().settag('table', True) self.columns = [] def process(self): "Set the columns on every row" index = 0 while index < len(self.contents): element = self.contents[index] if isinstance(element, Column): self.columns.append(element) del self.contents[index] elif isinstance(element, BlackBox): del self.contents[index] elif isinstance(element, Row): element.setcolumns(self.columns) index += 1 else: Trace.error('Unknown element type ' + element.__class__.__name__ + ' in table: ' + unicode(element.contents[0])) index += 1 class Row(Container): "A row in a table" def __init__(self): self.parser = TablePartParser() self.output = TaggedOutput().settag('tr', True) self.columns = list() def setcolumns(self, columns): "Process alignments for every column" if len(columns) != len(self.contents): Trace.error('Columns: ' + unicode(len(columns)) + ', cells: ' + unicode(len(self.contents))) return for index, cell in enumerate(self.contents): columns[index].set(cell) class Column(Container): "A column definition in a table" def __init__(self): self.parser = ColumnParser() self.output = EmptyOutput() def set(self, cell): "Set alignments in the corresponding cell" alignment = self.parameters['alignment'] if alignment == 'block': alignment = 'justify' cell.setattribute('align', alignment) valignment = self.parameters['valignment'] cell.setattribute('valign', valignment) class Cell(Container): "A cell in a table" def __init__(self): self.parser = TablePartParser() self.output = TaggedOutput().settag('td', True) def setmulticolumn(self, span): "Set the cell as multicolumn" self.setattribute('colspan', span) def setattribute(self, attribute, value): "Set a cell attribute in the tag" self.output.tag += ' ' + attribute + '="' + unicode(value) + '"' import struct import sys import os import os import os.path import codecs class Path(object): "Represents a generic path" def exists(self): "Check if the file exists" return os.path.exists(self.path) def open(self): "Open the file as readonly binary" return codecs.open(self.path, 'rb') def getmtime(self): "Return last modification time" return os.path.getmtime(self.path) def hasexts(self, exts): "Check if the file has one of the given extensions." for ext in exts: if self.hasext(ext): return True return False def hasext(self, ext): "Check if the file has the given extension" base, oldext = os.path.splitext(self.path) return oldext == ext def __unicode__(self): "Return a unicode string representation" return self.path def __eq__(self, path): "Compare to another path" if not hasattr(path, 'path'): return False return self.path == path.path class InputPath(Path): "Represents an input file" def __init__(self, url): "Create the input path based on url" self.url = url self.path = url if not os.path.isabs(url): self.path = os.path.join(Options.directory, url) class OutputPath(Path): "Represents an output file" def __init__(self, inputpath): "Create the output path based on an input path" self.url = inputpath.url if os.path.isabs(self.url): self.url = os.path.basename(self.url) self.path = os.path.join(Options.destdirectory, self.url) def changeext(self, ext): "Change extension to the given one" base, oldext = os.path.splitext(self.path) self.path = base + ext base, oldext = os.path.splitext(self.url) self.url = base + ext def exists(self): "Check if the file exists" return os.path.exists(self.path) def createdirs(self): "Create any intermediate directories that don't exist" dir = os.path.dirname(self.path) if len(dir) > 0 and not os.path.exists(dir): os.makedirs(dir) def removebackdirs(self): "Remove any occurrences of ../ (or ..\ on Windows)" self.path = os.path.normpath(self.path) backdir = '..' + os.path.sep while self.path.startswith(backdir): Trace.debug('Backdir in: ' + self.path) self.path = self.path[len(backdir):] while self.url.startswith('../'): Trace.debug('Backdir in: ' + self.url) self.url = self.url[len('../'):] class Image(Container): "An embedded image" ignoredtexts = ImageConfig.size['ignoredtexts'] vectorformats = ImageConfig.formats['vector'] rasterformats = ImageConfig.formats['raster'] defaultformat = ImageConfig.formats['default'] def __init__(self): self.parser = InsetParser() self.output = ImageOutput() self.type = 'embedded' self.width = None self.height = None self.maxwidth = None self.maxheight = None self.scale = None def process(self): "Place the url, convert the image if necessary." self.origin = InputPath(self.parameters['filename']) if not self.origin.exists(): Trace.error('Image ' + unicode(self.origin) + ' not found') return self.destination = self.getdestination(self.origin) self.setscale() ImageConverter.instance.convert(self) self.setsize() def getdestination(self, origin): "Convert origin path to destination path." "Changes extension of destination to output image format." destination = OutputPath(origin) forceformat = '.jpg' forcedest = Image.defaultformat if Options.forceformat: forceformat = Options.forceformat forcedest = Options.forceformat if not destination.hasext(forceformat): destination.changeext(forcedest) destination.removebackdirs() return destination def setscale(self): "Set the scale attribute if present." self.setifparam('scale') def setsize(self): "Set the size attributes width and height." imagefile = ImageFile(self.destination) width, height = imagefile.getdimensions() if width: self.maxwidth = unicode(width) + 'px' if self.scale: self.width = self.scalevalue(width) if height: self.maxheight = unicode(height) + 'px' if self.scale: self.height = self.scalevalue(height) self.setifparam('width') self.setifparam('height') def setifparam(self, name): "Set the value in the container if it exists as a param." if not name in self.parameters: return value = unicode(self.parameters[name]) for ignored in Image.ignoredtexts: if ignored in value: value = value.replace(ignored, '') setattr(self, name, value) def scalevalue(self, value): "Scale the value according to the image scale and return it as unicode." scaled = value * int(self.scale) / 100 return unicode(int(scaled)) + 'px' class ImageConverter(object): "A converter from one image file to another." active = True instance = None def convert(self, image): "Convert an image to PNG" if not ImageConverter.active: return if image.origin.url == image.destination.url: return if image.destination.exists(): if image.origin.getmtime() <= image.destination.getmtime(): # file has not changed; do not convert return image.destination.createdirs() command = 'convert ' params = self.getparams(image) for param in params: command += '-' + param + ' ' + unicode(params[param]) + ' ' command += '"' + unicode(image.origin) + '" "' command += unicode(image.destination) + '"' try: Trace.debug('ImageMagick Command: "' + command + '"') result = os.system(command.encode(sys.getfilesystemencoding())) if result != 0: Trace.error('ImageMagick not installed; images will not be processed') ImageConverter.active = False return Trace.message('Converted ' + unicode(image.origin) + ' to ' + unicode(image.destination)) except OSError, exception: Trace.error('Error while converting image ' + unicode(image.origin) + ': ' + unicode(exception)) def getparams(self, image): "Get the parameters for ImageMagick conversion" params = dict() if image.origin.hasexts(Image.vectorformats): scale = 100 if image.scale: scale = image.scale # descale image.scale = None params['density'] = scale elif image.origin.hasext('.pdf'): params['define'] = 'pdf:use-cropbox=true' return params ImageConverter.instance = ImageConverter() class ImageFile(object): "A file corresponding to an image (JPG or PNG)" dimensions = dict() def __init__(self, path): "Create the file based on its path" self.path = path def getdimensions(self): "Get the dimensions of a JPG or PNG image" if not self.path.exists(): return None, None if unicode(self.path) in ImageFile.dimensions: return ImageFile.dimensions[unicode(self.path)] dimensions = (None, None) if self.path.hasext('.png'): dimensions = self.getpngdimensions() elif self.path.hasext('.jpg'): dimensions = self.getjpgdimensions() ImageFile.dimensions[unicode(self.path)] = dimensions return dimensions def getpngdimensions(self): "Get the dimensions of a PNG image" pngfile = self.path.open() pngfile.seek(16) width = self.readlong(pngfile) height = self.readlong(pngfile) pngfile.close() return (width, height) def getjpgdimensions(self): "Get the dimensions of a JPEG image" jpgfile = self.path.open() start = self.readword(jpgfile) if start != int('ffd8', 16): Trace.error(unicode(self.path) + ' not a JPEG file') return (None, None) self.skipheaders(jpgfile, ['ffc0', 'ffc2']) self.seek(jpgfile, 3) height = self.readword(jpgfile) width = self.readword(jpgfile) jpgfile.close() return (width, height) def skipheaders(self, file, hexvalues): "Skip JPEG headers until one of the parameter headers is found" headervalues = [int(value, 16) for value in hexvalues] header = self.readword(file) safetycounter = 0 while header not in headervalues and safetycounter < 30: length = self.readword(file) if length == 0: Trace.error('End of file ' + file.name) return self.seek(file, length - 2) header = self.readword(file) safetycounter += 1 def readlong(self, file): "Read a long (32-bit) value from file" return self.readformat(file, '>L', 4) def readword(self, file): "Read a 16-bit value from file" return self.readformat(file, '>H', 2) def readformat(self, file, format, bytes): "Read any format from file" read = file.read(bytes) if read == '': Trace.error('EOF reached') return 0 tuple = struct.unpack(format, read) return tuple[0] def seek(self, file, bytes): "Seek forward, just by reading the given number of bytes" file.read(bytes) class ImageOutput(object): "Returns an image in the output" figure = TranslationConfig.constants['figure'] def gethtml(self, container): "Get the HTML output of the image as a list" html = ['' + ImageOutput.figure + ' ' + container.destination.url + '\n') return html class Float(Container): "A floating inset" def __init__(self): self.parser = InsetParser() self.output = TaggedOutput().settag('div class="float"', True) self.parentfloat = None self.children = [] self.number = None def process(self): "Get the float type." self.type = self.header[2] self.processfloats() self.processtags() def processtags(self): "Process the HTML tags." embeddedtag = self.getembeddedtag() wideningtag = self.getwideningtag() self.embed(embeddedtag + wideningtag) def processfloats(self): "Process all floats contained inside." floats = self.searchall(Float) for float in floats: float.output.tag = float.output.tag.replace('div', 'span') float.parentfloat = self self.children.append(float) def getembeddedtag(self): "Get the tag for the embedded object." floats = self.searchall(Float) if len(floats) > 0: return 'div class="multi' + self.type + '"' return 'div class="' + self.type + '"' def getwideningtag(self): "Get the tag to set float width, if present." images = self.searchall(Image) if len(images) != 1: return '' image = images[0] if not image.width: return '' if not '%' in image.width: return '' image.type = 'figure' width = image.width image.width = None return ' style="max-width: ' + width + ';"' def embed(self, tag): "Embed the whole contents in a div" tagged = TaggedText().complete(self.contents, tag, True) self.contents = [tagged] def searchinside(self, contents, type): "Search for a given type in the contents" list = [] for element in contents: list += self.searchinelement(element, type) return list def searchinelement(self, element, type): "Search for a given type outside floats" if isinstance(element, Float): return [] if isinstance(element, type): return [element] return self.searchinside(element.contents, type) def __unicode__(self): "Return a printable representation" return 'Floating inset of type ' + self.type class Wrap(Float): "A wrapped (floating) float" def processtags(self): "Add the widening tag to the parent tag." embeddedtag = self.getembeddedtag() self.embed(embeddedtag) placement = self.parameters['placement'] wideningtag = self.getwideningtag() self.output.tag = 'div class="wrap-' + placement + '"' + wideningtag class Caption(Container): "A caption for a figure or a table" def __init__(self): self.parser = InsetParser() self.output = TaggedOutput().settag('div class="caption"', True) class Listing(Float): "A code listing" def __init__(self): Float.__init__(self) self.numbered = None self.counter = 0 def process(self): "Remove all layouts" self.parselstparams() self.type = 'listing' captions = self.searchremove(Caption) newcontents = [] for container in self.contents: newcontents += self.extract(container) tagged = TaggedText().complete(newcontents, 'code class="listing"', False) self.contents = [TaggedText().complete(captions + [tagged], 'div class="listing"', True)] def processparams(self): "Process listing parameteres." self.parselstparams() if not 'numbers' in self.parameters: return self.numbered = self.parameters['numbers'] def extract(self, container): "Extract the container's contents and return them" if isinstance(container, StringContainer): return self.modifystring(container) if isinstance(container, StandardLayout): return self.modifylayout(container) if isinstance(container, PlainLayout): return self.modifylayout(container) Trace.error('Unexpected container ' + container.__class__.__name__ + ' in listing') container.tree() return [] def modifystring(self, string): "Modify a listing string" if string.string == '': string.string = u'​' return self.modifycontainer(string) def modifylayout(self, layout): "Modify a standard layout" if len(layout.contents) == 0: layout.contents = [Constant(u'​')] return self.modifycontainer(layout) def modifycontainer(self, container): "Modify a listing container" contents = [container, Constant('\n')] if self.numbered: self.counter += 1 tag = 'span class="number-' + self.numbered + '"' contents.insert(0, TaggedText().constant(unicode(self.counter), tag)) return contents class PostFloat(object): "Postprocess a float: number it and move the label" processedclass = Float def postprocess(self, last, float, next): "Move the label to the top and number the caption" captions = float.searchinside(float.contents, Caption) for caption in captions: self.postlabels(float, caption) self.postnumber(caption, float) return float def postlabels(self, float, caption): "Search for labels and move them to the top" labels = caption.searchremove(Label) if len(labels) == 0: return float.contents = labels + float.contents def postnumber(self, caption, float): "Number the caption" self.numberfloat(float) caption.contents.insert(0, Constant(float.entry + u' ')) def numberfloat(self, float): "Number a float if it isn't numbered" if float.number: return if float.parentfloat: self.numberfloat(float.parentfloat) index = float.parentfloat.children.index(float) float.number = NumberGenerator.letters[index + 1].lower() float.entry = '(' + float.number + ')' else: float.number = NumberGenerator.instance.generatechaptered(float.type) float.entry = TranslationConfig.floats[float.type] + float.number class PostWrap(PostFloat): "For a wrap: exactly like a float" processedclass = Wrap class PostListing(PostFloat): "For a listing: exactly like a float" processedclass = Listing Postprocessor.stages += [PostFloat, PostWrap, PostListing] import sys class FormulaEquation(CommandBit): "A simple numbered equation." piece = 'equation' def parsebit(self, pos): "Parse the array" self.output = ContentsOutput() inner = WholeFormula() inner.parsebit(pos) self.add(inner) class FormulaCell(FormulaCommand): "An array cell inside a row" def __init__(self, alignment): FormulaCommand.__init__(self) self.alignment = alignment self.output = TaggedOutput().settag('td class="formula-' + alignment +'"', True) def parsebit(self, pos): formula = WholeFormula() if not formula.detect(pos): Trace.error('Unexpected end of array cell at ' + pos.remaining()) pos.skip(pos.current()) return formula.parsebit(pos) self.add(formula) class FormulaRow(FormulaCommand): "An array row inside an array" cellseparator = FormulaConfig.array['cellseparator'] def __init__(self, alignments): FormulaCommand.__init__(self) self.alignments = alignments self.output = TaggedOutput().settag('tr', True) def parsebit(self, pos): "Parse a whole row" index = 0 pos.pushending(FormulaRow.cellseparator, optional=True) while not pos.finished(): alignment = self.alignments[index % len(self.alignments)] cell = FormulaCell(alignment) cell.parsebit(pos) self.add(cell) index += 1 pos.checkskip(FormulaRow.cellseparator) return for cell in self.iteratecells(pos): cell.parsebit(pos) self.add(cell) def iteratecells(self, pos): "Iterate over all cells, finish when count ends" for index, alignment in enumerate(self.alignments): if self.anybutlast(index): pos.pushending(cellseparator) yield FormulaCell(alignment) if self.anybutlast(index): if not pos.checkfor(cellseparator): Trace.error('No cell separator ' + cellseparator) else: self.original += pos.popending(cellseparator) def anybutlast(self, index): "Return true for all cells but the last" return index < len(self.alignments) - 1 class Environment(CommandBit): "A \\begin{}...\\end environment with rows and cells." def parsebit(self, pos): "Parse the whole environment." self.output = TaggedOutput().settag('table class="' + self.piece + '"', True) self.alignments = ['l'] self.parserows(pos) def parserows(self, pos): "Parse all rows, finish when no more row ends" for row in self.iteraterows(pos): row.parsebit(pos) self.add(row) def iteraterows(self, pos): "Iterate over all rows, end when no more row ends" rowseparator = FormulaConfig.array['rowseparator'] while True: pos.pushending(rowseparator, True) yield FormulaRow(self.alignments) if pos.checkfor(rowseparator): self.original += pos.popending(rowseparator) else: return class FormulaArray(Environment): "An array within a formula" piece = 'array' def parsebit(self, pos): "Parse the array" self.output = TaggedOutput().settag('table class="formula"', True) self.parsealignments(pos) self.parserows(pos) def parsealignments(self, pos): "Parse the different alignments" # vertical self.valign = 'c' vbracket = SquareBracket() if vbracket.detect(pos): vbracket.parseliteral(pos) self.valign = vbracket.literal self.add(vbracket) # horizontal bracket = Bracket().parseliteral(pos) self.add(bracket) self.alignments = [] for l in bracket.literal: self.alignments.append(l) class FormulaCases(Environment): "A cases statement" piece = 'cases' def parsebit(self, pos): "Parse the cases" self.output = TaggedOutput().settag('table class="cases"', True) self.alignments = ['l', 'l'] self.parserows(pos) class FormulaAlign(Environment): "A number of aligned formulae" piece = 'align' def parsebit(self, pos): "Parse the aligned bits" self.output = TaggedOutput().settag('table class="align"', True) self.alignments = ['r'] self.parserows(pos) class BeginCommand(CommandBit): "A \\begin{}...\end command and what it entails (array, cases, aligned)" commandmap = {FormulaConfig.array['begin']:''} innerbits = [FormulaEquation(), FormulaArray(), FormulaCases(), FormulaAlign()] def parsebit(self, pos): "Parse the begin command" bracket = Bracket().parseliteral(pos) self.original += bracket.literal bit = self.findbit(bracket.literal) ending = FormulaConfig.array['end'] + '{' + bracket.literal + '}' pos.pushending(ending) bit.parsebit(pos) self.add(bit) self.original += pos.popending(ending) def findbit(self, piece): "Find the command bit corresponding to the \\begin{piece}" for bit in BeginCommand.innerbits: if bit.piece == piece or bit.piece + '*' == piece: newbit = Cloner.clone(bit) return newbit bit = Environment() bit.piece = piece return bit FormulaCommand.commandbits += [BeginCommand()] import os import sys import codecs class BulkFile(object): "A file to treat in bulk" def __init__(self, filename): self.filename = filename self.temp = self.filename + '.temp' def readall(self): "Read the whole file" for encoding in FileConfig.parsing['encodings']: try: return self.readcodec(encoding) except UnicodeDecodeError: pass Trace.error('No suitable encoding for ' + self.filename) return [] def readcodec(self, encoding): "Read the whole file with the given encoding" filein = codecs.open(self.filename, 'r', encoding) lines = filein.readlines() filein.close() return lines def getfiles(self): "Get reader and writer for a file name" reader = LineReader(self.filename) writer = LineWriter(self.temp) return reader, writer def swaptemp(self): "Swap the temp file for the original" os.chmod(self.temp, os.stat(self.filename).st_mode) os.rename(self.temp, self.filename) def __unicode__(self): "Get the unicode representation" return 'file ' + self.filename class BibTeX(Container): "Show a BibTeX bibliography and all referenced entries" def __init__(self): self.parser = InsetParser() self.output = ContentsOutput() def process(self): "Read all bibtex files and process them" self.entries = [] bibliography = TranslationConfig.constants['bibliography'] tag = TaggedText().constant(bibliography, 'h1 class="biblio"') self.contents.append(tag) files = self.parameters['bibfiles'].split(',') for file in files: bibfile = BibFile(file) bibfile.parse() self.entries += bibfile.entries Trace.message('Parsed ' + unicode(bibfile)) self.entries.sort(key = unicode) self.applystyle() def applystyle(self): "Read the style and apply it to all entries" style = self.readstyle() for entry in self.entries: entry.template = style['default'] if entry.type in style: entry.template = style[entry.type] entry.process() self.contents.append(entry) def readstyle(self): "Read the style from the bibliography options" options = self.parameters['options'].split(',') for option in options: if hasattr(BibStylesConfig, option): return getattr(BibStylesConfig, option) return BibStylesConfig.default class BibFile(object): "A BibTeX file" def __init__(self, filename): "Create the BibTeX file" self.filename = filename + '.bib' self.added = 0 self.ignored = 0 self.entries = [] def parse(self): "Parse the BibTeX file" bibpath = InputPath(self.filename) bibfile = BulkFile(bibpath.path) parsed = list() for line in bibfile.readall(): line = line.strip() if not line.startswith('%') and not line == '': parsed.append(line) self.parseentries('\n'.join(parsed)) def parseentries(self, text): "Extract all the entries in a piece of text" pos = Position(text) pos.skipspace() while not pos.finished(): self.parseentry(pos) def parseentry(self, pos): "Parse a single entry" for entry in Entry.entries: if entry.detect(pos): newentry = Cloner.clone(entry) newentry.parse(pos) if newentry.isreferenced(): self.entries.append(newentry) self.added += 1 else: self.ignored += 1 return # Skip the whole line, and show it as an error pos.checkskip('\n') Entry.entries[0].lineerror('Unidentified entry', pos) def __unicode__(self): "String representation" string = self.filename + ': ' + unicode(self.added) + ' entries added, ' string += unicode(self.ignored) + ' entries ignored' return string class Entry(Container): "An entry in a BibTeX file" entries = [] structure = ['{', ',', '=', '"'] quotes = ['{', '"', '#'] def __init__(self): self.key = None self.tags = dict() self.output = TaggedOutput().settag('p class="biblio"') def parse(self, pos): "Parse the entry between {}" self.type = self.parsepiece(pos, Entry.structure) pos.skipspace() if not pos.checkskip('{'): self.lineerror('Entry should start with {', pos) return pos.pushending('}') self.parsetags(pos) pos.popending('}') pos.skipspace() def parsetags(self, pos): "Parse all tags in the entry" pos.skipspace() while not pos.finished(): if pos.checkskip('{'): self.lineerror('Unmatched {', pos) return self.parsetag(pos) def parsetag(self, pos): piece = self.parsepiece(pos, Entry.structure) if pos.checkskip(','): self.key = piece return if pos.checkskip('='): piece = piece.lower().strip() pos.skipspace() value = self.parsevalue(pos) self.tags[piece] = value pos.skipspace() if not pos.finished() and not pos.checkskip(','): self.lineerror('Missing , in BibTeX tag', pos) return def parsevalue(self, pos): "Parse the value for a tag" pos.skipspace() if pos.checkfor(','): self.lineerror('Unexpected ,', pos) return '' if pos.checkfor('{'): return self.parsebracket(pos) elif pos.checkfor('"'): return self.parsequoted(pos) else: return self.parsepiece(pos, Entry.structure) def parsebracket(self, pos): "Parse a {} bracket" if not pos.checkskip('{'): self.lineerror('Missing opening { in bracket', pos) return '' pos.pushending('}') bracket = self.parserecursive(pos) pos.popending('}') return bracket def parsequoted(self, pos): "Parse a piece of quoted text" if not pos.checkskip('"'): self.lineerror('Missing opening " in quote', pos) return pos.pushending('"', True) quoted = self.parserecursive(pos) pos.popending('"') pos.skipspace() if pos.checkskip('#'): pos.skipspace() quoted += self.parsequoted(pos) return quoted def parserecursive(self, pos): "Parse brackets or quotes recursively" piece = '' while not pos.finished(): piece += self.parsepiece(pos, Entry.quotes) if not pos.finished(): if pos.checkfor('{'): piece += self.parsebracket(pos) elif pos.checkfor('"'): piece += self.parsequoted(pos) else: self.lineerror('Missing opening { or "', pos) return piece return piece def parsepiece(self, pos, undesired): "Parse a piece not structure" return pos.glob(lambda current: not current in undesired) def lineerror(self, message, pos): "Show an error message for a line." toline = pos.globexcluding('\n') pos.checkskip('\n') Trace.error(message + ': ' + toline) class SpecialEntry(Entry): "A special entry" types = ['@STRING', '@PREAMBLE', '@COMMENT'] def detect(self, pos): "Detect the special entry" for type in SpecialEntry.types: if pos.checkfor(type): return True return False def isreferenced(self): "A special entry is never referenced" return False def __unicode__(self): "Return a string representation" return self.type class PubEntry(Entry): "A publication entry" def detect(self, pos): "Detect a publication entry" return pos.checkfor('@') def isreferenced(self): "Check if the entry is referenced" if not self.key: return False return self.key in BiblioEntry.entries def process(self): "Process the entry" biblio = BiblioEntry() biblio.processcites(self.key) self.contents = [biblio, Constant(' ')] self.contents.append(self.getcontents()) def getcontents(self): "Get the contents as a constant" contents = self.template while contents.find('$') >= 0: tag = self.extracttag(contents) value = self.gettag(tag) contents = contents.replace('$' + tag, value) return Constant(contents) def extracttag(self, string): "Extract the first tag in the form $tag" pos = Position(string) pos.globexcluding('$') pos.skip('$') return pos.globalpha() def __unicode__(self): "Return a string representation" string = '' author = self.gettag('author') if author: string += author + ': ' title = self.gettag('title') if title: string += '"' + title + '"' return string def gettag(self, key): "Get a tag with the given key" if not key in self.tags: return '' return self.tags[key] Entry.entries += [SpecialEntry(), PubEntry()] class ContainerFactory(object): "Creates containers depending on the first line" def __init__(self): "Read table that convert start lines to containers" types = dict() for start, typename in ContainerConfig.starts.iteritems(): types[start] = globals()[typename] self.tree = ParseTree(types) def createcontainer(self, reader): "Parse a single container." #Trace.debug('processing "' + reader.currentline().strip() + '"') if reader.currentline() == '': reader.nextline() return None type = self.tree.find(reader) container = type.__new__(type) container.__init__() container.start = reader.currentline().strip() self.parse(container, reader) return container def parse(self, container, reader): "Parse a container" parser = container.parser parser.parent = container parser.ending = self.getending(container) parser.factory = self container.header = parser.parseheader(reader) container.begin = parser.begin container.contents = parser.parse(reader) container.parameters = parser.parameters container.process() container.parser = None def getending(self, container): "Get the ending for a container" split = container.start.split() if len(split) == 0: return None start = split[0] if start in ContainerConfig.startendings: return ContainerConfig.startendings[start] classname = container.__class__.__name__ if classname in ContainerConfig.endings: return ContainerConfig.endings[classname] if hasattr(container, 'ending'): Trace.error('Pending ending in ' + container.__class__.__name__) return container.ending return None class ParseTree(object): "A parsing tree" default = '~~default~~' def __init__(self, types): "Create the parse tree" self.root = dict() for start, type in types.iteritems(): self.addstart(type, start) def addstart(self, type, start): "Add a start piece to the tree" tree = self.root for piece in start.split(): if not piece in tree: tree[piece] = dict() tree = tree[piece] if ParseTree.default in tree: Trace.error('Start ' + start + ' duplicated') tree[ParseTree.default] = type def find(self, reader): "Find the current sentence in the tree" branches = [self.root] for piece in reader.currentline().split(): current = branches[-1] piece = piece.rstrip('>') if piece in current: branches.append(current[piece]) while not ParseTree.default in branches[-1]: Trace.error('Line ' + reader.currentline().strip() + ' not found') branches.pop() last = branches[-1] return last[ParseTree.default] class TOCEntry(Container): "A container for a TOC entry." copied = [StringContainer, Constant, Space] allowed = [ TextFamily, EmphaticText, VersalitasText, BarredText, SizeText, ColorText, LangLine, Formula ] def header(self, container): "Create a TOC entry for header and footer (0 depth)." self.depth = 0 self.output = EmptyOutput() return self def create(self, container): "Create the TOC entry for a container, consisting of a single link." self.entry = container.entry self.branches = [] text = container.entry + ':' labels = container.searchall(Label) if len(labels) == 0 or Options.toc: url = Options.toctarget + '#toc-' + container.type + '-' + container.number link = Link().complete(text, url=url) else: label = labels[0] link = Link().complete(text) link.destination = label self.contents = [link] if container.number == '': link.contents.append(Constant(u' ')) link.contents += self.gettitlecontents(container) self.output = TaggedOutput().settag('div class="toc"', True) self.depth = container.level self.partkey = container.partkey return self def gettitlecontents(self, container): "Get the title of the container." shorttitles = container.searchall(ShortTitle) if len(shorttitles) > 0: contents = [Constant(u' ')] for shorttitle in shorttitles: contents += shorttitle.contents return contents return self.safeclone(container).contents def safeclone(self, container): "Return a new container with contents only in a safe list, recursively." clone = Cloner.clone(container) clone.output = container.output clone.contents = [] for element in container.contents: if element.__class__ in TOCEntry.copied: clone.contents.append(element) elif element.__class__ in TOCEntry.allowed: clone.contents.append(self.safeclone(element)) return clone def __unicode__(self): "Return a printable representation." return 'TOC entry: ' + self.entry class Indenter(object): "Manages and writes indentation for the TOC." def __init__(self): self.depth = 0 def getindent(self, depth): indent = '' if depth > self.depth: indent = self.openindent(depth - self.depth) elif depth < self.depth: indent = self.closeindent(self.depth - depth) self.depth = depth return Constant(indent) def openindent(self, times): "Open the indenting div a few times." indent = '' for i in range(times): indent += '
\n' return indent def closeindent(self, times): "Close the indenting div a few times." indent = '' for i in range(times): indent += '
\n' return indent class TOCTree(object): "A tree that contains the full TOC." def __init__(self): self.tree = [] self.branches = [] def store(self, entry): "Place the entry in a tree of entries." while len(self.tree) < entry.depth: self.tree.append(None) if len(self.tree) > entry.depth: self.tree = self.tree[:entry.depth] stem = self.findstem() if len(self.tree) == 0: self.branches.append(entry) self.tree.append(entry) if stem: entry.stem = stem stem.branches.append(entry) def findstem(self): "Find the stem where our next element will be inserted." for element in reversed(self.tree): if element: return element return None class TOCConverter(object): "A converter from containers to TOC entries." cache = dict() tree = TOCTree() def __init__(self): self.indenter = Indenter() def translate(self, container): "Translate a container to TOC entry + indentation." entry = self.convert(container) if not entry: return [] indent = self.indenter.getindent(entry.depth) return [indent, entry] def convert(self, container): "Convert a container to a TOC entry." if container.__class__ in [LyXHeader, LyXFooter]: return TOCEntry().header(container) if not hasattr(container, 'partkey'): return None if container.partkey in self.cache: return TOCConverter.cache[container.partkey] if container.level > LyXHeader.tocdepth: return None entry = TOCEntry().create(container) TOCConverter.cache[container.partkey] = entry TOCConverter.tree.store(entry) return entry import os.path class Basket(object): "A basket to place a set of containers. Can write them, store them..." def setwriter(self, writer): self.writer = writer return self class WriterBasket(Basket): "A writer of containers. Just writes them out to a writer." def write(self, container): "Write a container to the line writer." self.writer.write(container.gethtml()) def finish(self): "Mark as finished." self.writer.close() class KeeperBasket(Basket): "Keeps all containers stored." def __init__(self): self.contents = [] def write(self, container): "Keep the container." self.contents.append(container) def finish(self): "Finish the basket by flushing to disk." self.flush() def flush(self): "Flush the contents to the writer." for container in self.contents: self.writer.write(container.gethtml()) class TOCBasket(Basket): "A basket to place the TOC of a document." def __init__(self): self.converter = TOCConverter() def setwriter(self, writer): Basket.setwriter(self, writer) Options.nocopy = True self.writer.write(LyXHeader().gethtml()) return self def write(self, container): "Write the table of contents for a container." entries = self.converter.translate(container) for entry in entries: self.writer.write(entry.gethtml()) def finish(self): "Mark as finished." self.writer.write(LyXFooter().gethtml()) class IntegralProcessor(object): "A processor for an integral document." def __init__(self): "Create the processor for the integral contents." self.storage = [] def locate(self, container): "Locate only containers of the processed type." return isinstance(container, self.processedtype) def store(self, container): "Store a new container." self.storage.append(container) def process(self): "Process the whole storage." for container in self.storage: self.processeach(container) class IntegralLayout(IntegralProcessor): "A processor for layouts that will appear in the TOC." processedtype = Layout tocentries = [] def processeach(self, layout): "Keep only layouts that have an entry." if not hasattr(layout, 'entry'): return IntegralLayout.tocentries.append(layout) class IntegralTOC(IntegralProcessor): "A processor for an integral TOC." processedtype = TableOfContents def processeach(self, toc): "Fill in a Table of Contents." toc.output = TaggedOutput().settag('div class="fulltoc"', True) converter = TOCConverter() for container in IntegralLayout.tocentries: toc.contents += converter.translate(container) # finish off with the footer to align indents toc.contents += converter.translate(LyXFooter()) def writetotoc(self, entries, toc): "Write some entries to the TOC." for entry in entries: toc.contents.append(entry) class IntegralBiblioEntry(IntegralProcessor): "A processor for an integral bibliography entry." processedtype = BiblioEntry def processeach(self, entry): "Process each entry." number = NumberGenerator.instance.generateunique('integralbib') link = Link().complete(number, 'biblio-' + number, type='biblioentry') entry.contents = [Constant('['), link, Constant('] ')] if entry.key in BiblioCite.cites: for cite in BiblioCite.cites[entry.key]: cite.complete(number, anchor = 'cite-' + number) cite.destination = link class IntegralFloat(IntegralProcessor): "Store all floats in the document by type." processedtype = Float bytype = dict() def processeach(self, float): "Store each float by type." if not float.type in IntegralFloat.bytype: IntegralFloat.bytype[float.type] = [] IntegralFloat.bytype[float.type].append(float) class IntegralListOf(IntegralProcessor): "A processor for an integral list of floats." processedtype = ListOf def processeach(self, listof): "Fill in a list of floats." listof.output = TaggedOutput().settag('div class="fulltoc"', True) if not listof.type in IntegralFloat.bytype: Trace.message('No floats of type ' + listof.type) return for float in IntegralFloat.bytype[listof.type]: entry = self.processfloat(float) if entry: listof.contents.append(entry) def processfloat(self, float): "Get an entry for the list of floats." if float.parentfloat: return None link = self.createlink(float) if not link: return None return TaggedText().complete([link], 'div class="toc"', True) def createlink(self, float): "Create the link to the float label." captions = float.searchinside(float.contents, Caption) if len(captions) == 0: return None labels = float.searchinside(float.contents, Label) if len(labels) > 0: label = labels[0] else: label = Label().create(' ', float.entry.replace(' ', '-')) float.contents.insert(0, label) labels.append(label) if len(labels) > 1: Trace.error('More than one label in ' + float.entry) link = Link().complete(float.entry + u': ') for caption in captions: link.contents += caption.contents[1:] link.destination = label return link class IntegralReference(IntegralProcessor): "A processor for a reference to a label." processedtype = Reference def processeach(self, reference): "Extract the text of the original label." text = self.extracttext(reference.destination) if text: reference.contents.insert(0, Constant(text)) def extracttext(self, container): "Extract the final text for the label." while not hasattr(container, 'number'): if not hasattr(container, 'parent'): # no number; label must be in some unnumbered element return None container = container.parent return container.number class MemoryBasket(KeeperBasket): "A basket which stores everything in memory, processes it and writes it." def __init__(self): "Create all processors in one go." KeeperBasket.__init__(self) self.processors = [ IntegralLayout(), IntegralTOC(), IntegralBiblioEntry(), IntegralFloat(), IntegralListOf(), IntegralReference() ] def finish(self): "Process everything which cannot be done in one pass and write to disk." self.process() self.flush() def process(self): "Process everything with the integral processors." self.searchintegral() for processor in self.processors: processor.process() def searchintegral(self): "Search for all containers for all integral processors." for container in self.contents: # container.tree() if self.integrallocate(container): self.integralstore(container) container.locateprocess(self.integrallocate, self.integralstore) def integrallocate(self, container): "Locate all integrals." for processor in self.processors: if processor.locate(container): return True return False def integralstore(self, container): "Store a container in one or more processors." for processor in self.processors: if processor.locate(container): processor.store(container) class IntegralLink(IntegralProcessor): "Integral link processing for multi-page output." processedtype = Link def processeach(self, link): "Process each link and add the current page." link.page = self.page class SplittingBasket(Basket): "A basket used to split the output in different files." baskets = [] def setwriter(self, writer): if not hasattr(writer, 'filename') or not writer.filename: Trace.error('Cannot use standard output for split output; ' + 'please supply an output filename.') exit() self.writer = writer self.base, self.extension = os.path.splitext(writer.filename) self.converter = TOCConverter() self.basket = MemoryBasket() return self def write(self, container): "Write a container, possibly splitting the file." self.basket.write(container) def finish(self): "Process the whole basket, create page baskets and flush all of them." self.basket.process() basket = self.addbasket(self.writer) for container in self.basket.contents: if self.mustsplit(container): filename = self.getfilename(container) Trace.debug('New page ' + filename) basket.write(LyXFooter()) basket = self.addbasket(LineWriter(filename)) basket.write(LyXHeader()) basket.write(container) for basket in self.baskets: basket.process() for basket in self.baskets: basket.flush() def addbasket(self, writer): "Add a new basket." basket = MemoryBasket() basket.setwriter(writer) self.baskets.append(basket) # set the page name everywhere basket.page = writer.filename integrallink = IntegralLink() integrallink.page = os.path.basename(basket.page) basket.processors = [integrallink] return basket def mustsplit(self, container): "Find out if the oputput file has to be split at this entry." if self.splitalone(container): return True if not hasattr(container, 'entry'): return False entry = self.converter.convert(container) if not entry: return False if hasattr(entry, 'split'): return True return entry.depth <= Options.splitpart def splitalone(self, container): "Find out if the container must be split in its own page." found = [] container.locateprocess( lambda container: container.__class__ in [PrintNomenclature, PrintIndex], lambda container: found.append(container.__class__.__name__)) if not found: return False container.depth = 0 container.split = found[0].lower().replace('print', '') return True def getfilename(self, container): "Get the new file name for a given container." if hasattr(container, 'split'): partname = '-' + container.split else: entry = self.converter.convert(container) if entry.depth == Options.splitpart: partname = '-' + container.number else: partname = '-' + container.type + '-' + container.number return self.base + partname + self.extension class PendingList(object): "A pending list" def __init__(self): self.contents = [] self.type = None def additem(self, item): "Add a list item" self.contents += item.contents if not self.type: self.type = item.type def adddeeper(self, deeper): "Add a deeper list item" if self.empty(): self.insertfake() item = self.contents[-1] self.contents[-1].contents += deeper.contents def generate(self): "Get the resulting list" if not self.type: tag = 'ul' else: tag = TagConfig.listitems[self.type] text = TaggedText().complete(self.contents, tag, True) self.__init__() return text def isduewithitem(self, item): "Decide whether the pending list must be generated before the given item" if not self.type: return False if self.type != item.type: return True return False def isduewithnext(self, next): "Applies only if the list is finished with next item." if not next: return True if not isinstance(next, ListItem) and not isinstance(next, DeeperList): return True return False def empty(self): return len(self.contents) == 0 def insertfake(self): "Insert a fake item" item = TaggedText().constant('', 'li class="nested"', True) self.contents = [item] self.type = 'Itemize' def __unicode__(self): result = 'pending ' + unicode(self.type) + ': [' for element in self.contents: result += unicode(element) + ', ' if len(self.contents) > 0: result = result[:-2] return result + ']' class PostListItem(object): "Postprocess a list item" processedclass = ListItem def postprocess(self, last, item, next): "Add the item to pending and return an empty item" if not hasattr(self.postprocessor, 'list'): self.postprocessor.list = PendingList() self.postprocessor.list.additem(item) if self.postprocessor.list.isduewithnext(next): return self.postprocessor.list.generate() if isinstance(next, ListItem) and self.postprocessor.list.isduewithitem(next): return self.postprocessor.list.generate() return BlackBox() class PostDeeperList(object): "Postprocess a deeper list" processedclass = DeeperList def postprocess(self, last, deeper, next): "Append to the list in the postprocessor" if not hasattr(self.postprocessor, 'list'): self.postprocessor.list = PendingList() self.postprocessor.list.adddeeper(deeper) if self.postprocessor.list.isduewithnext(next): return self.postprocessor.list.generate() return BlackBox() Postprocessor.stages += [PostListItem, PostDeeperList] class PostTable(object): "Postprocess a table" processedclass = Table def postprocess(self, last, table, next): "Postprocess a table: long table, multicolumn rows" self.longtable(table) for row in table.contents: index = 0 while index < len(row.contents): self.checkmulticolumn(row, index) index += 1 return table def longtable(self, table): "Postprocess a long table, removing unwanted rows" if not 'features' in table.parameters: return features = table.parameters['features'] if not 'islongtable' in features: return if features['islongtable'] != 'true': return if self.hasrow(table, 'endfirsthead'): self.removerows(table, 'endhead') if self.hasrow(table, 'endlastfoot'): self.removerows(table, 'endfoot') def hasrow(self, table, attrname): "Find out if the table has a row of first heads" for row in table.contents: if attrname in row.parameters: return True return False def removerows(self, table, attrname): "Remove the head rows, since the table has first head rows." for row in table.contents: if attrname in row.parameters: row.output = EmptyOutput() def checkmulticolumn(self, row, index): "Process a multicolumn attribute" cell = row.contents[index] if not hasattr(cell, 'parameters') or not 'multicolumn' in cell.parameters: return mc = cell.parameters['multicolumn'] if mc != '1': Trace.error('Unprocessed multicolumn=' + unicode(multicolumn) + ' cell ' + unicode(cell)) return total = 1 index += 1 while self.checkbounds(row, index): del row.contents[index] total += 1 cell.setmulticolumn(total) def checkbounds(self, row, index): "Check if the index is within bounds for the row" if index >= len(row.contents): return False if not 'multicolumn' in row.contents[index].parameters: return False if row.contents[index].parameters['multicolumn'] != '2': return False return True Postprocessor.stages.append(PostTable) class PostFormula(object): "Postprocess a formula" processedclass = Formula def postprocess(self, last, formula, next): "Postprocess any formulae" self.postnumbering(formula) self.postcontents(formula.contents) self.posttraverse(formula) return formula def postnumbering(self, formula): "Check if it's a numbered equation, insert number." if formula.header[0] != 'numbered': return formula.number = NumberGenerator.instance.generatechaptered('formula') formula.entry = '(' + formula.number + ')' functions = formula.searchremove(LabelFunction) if len(functions) > 1: Trace.error('More than one label in ' + unicode(formula)) return if len(functions) == 0: label = Label() label.create(formula.entry + ' ', 'eq-' + formula.number, type="eqnumber") else: label = functions[0].label label.complete(formula.entry + ' ') label.parent = formula formula.contents.insert(0, label) def postcontents(self, contents): "Search for sum or integral" for index, bit in enumerate(contents): self.checklimited(contents, index) if isinstance(bit, FormulaBit): self.postcontents(bit.contents) def checklimited(self, contents, index): "Check for a command with limits" bit = contents[index] if not isinstance(bit, EmptyCommand): return if not bit.command in FormulaConfig.limits['commands']: return limits = self.findlimits(contents, index + 1) limits.reverse() if len(limits) == 0: return tagged = TaggedBit().complete(limits, 'span class="limits"') contents.insert(index + 1, tagged) def findlimits(self, contents, index): "Find the limits for the command" limits = [] while index < len(contents): if not self.checklimits(contents, index): return limits limits.append(contents[index]) del contents[index] return limits def checklimits(self, contents, index): "Check for a command making the limits" bit = contents[index] if not isinstance(bit, SymbolFunction): return False if not bit.command in FormulaConfig.limits['operands']: return False bit.output.tag += ' class="bigsymbol"' return True def posttraverse(self, formula): "Traverse over the contents to alter variables and space units." flat = self.flatten(formula) last = None for bit, contents in self.traverse(flat): if bit.type == 'alpha': self.italicize(bit, contents) elif bit.type == 'font' and last and last.type == 'number': bit.contents.insert(0, FormulaConstant(u' ')) # last.contents.append(FormulaConstant(u' ')) last = bit def flatten(self, bit): "Return all bits as a single list of (bit, list) pairs." flat = [] for element in bit.contents: if element.type: flat.append((element, bit.contents)) elif isinstance(element, FormulaBit): flat += self.flatten(element) return flat def traverse(self, flattened): "Traverse each (bit, list) pairs of the formula." for element in flattened: yield element def italicize(self, bit, contents): "Italicize the given bit of text." index = contents.index(bit) contents[index] = TaggedBit().complete([bit], 'i') Postprocessor.stages.append(PostFormula) class eLyXerConverter(object): "Converter for a document in a lyx file. Places all output in a given basket." def __init__(self): self.filtering = False def setio(self, ioparser): "Set the InOutParser" self.reader = ioparser.getreader() self.basket = self.getbasket() self.basket.setwriter(ioparser.getwriter()) return self def getbasket(self): "Get the appropriate basket for the current options." if Options.toc: return TOCBasket() if Options.splitpart: return SplittingBasket() if Options.memory: return MemoryBasket() return WriterBasket() def embed(self, reader): "Embed the results from a reader into a memory basket." "Header and footer are ignored. Useful for embedding one document inside another." self.filtering = True self.reader = reader self.basket = MemoryBasket() self.writer = NullWriter() return self def convert(self): "Perform the conversion for the document" try: self.processcontents() except (Exception): Trace.error('Conversion failed at ' + self.reader.currentline()) raise def processcontents(self): "Parse the contents and write it by containers" factory = ContainerFactory() self.postproc = Postprocessor() while not self.reader.finished(): container = factory.createcontainer(self.reader) if container and not self.filtered(container): result = self.postproc.postprocess(container) if result: self.basket.write(result) # last round: clear the pipeline result = self.postproc.postprocess(None) if result: self.basket.write(result) if not self.filtering: self.basket.finish() def filtered(self, container): "Find out if the container is a header or footer and must be filtered." if not self.filtering: return False if container.__class__ in [LyXHeader, LyXFooter]: return True return False def getcontents(self): "Return the contents of the basket." return self.basket.contents def __unicode__(self): "Printable representation." string = 'Converter with filtering ' + unicode(self.filtering) string += ' and basket ' + unicode(self.basket) return string class InOutParser(object): "Parse in and out arguments" def __init__(self): self.filein = sys.stdin self.fileout = sys.stdout def parse(self, args): "Parse command line arguments" self.filein = sys.stdin self.fileout = sys.stdout if len(args) < 2: Trace.quietmode = True if len(args) > 0: self.filein = args[0] del args[0] self.readdir(self.filein, 'directory') else: Options.directory = '.' if len(args) > 0: self.fileout = args[0] del args[0] self.readdir(self.fileout, 'destdirectory') else: Options.destdirectory = '.' if len(args) > 0: raise Exception('Unused arguments: ' + unicode(args)) return self def getreader(self): "Get the resulting reader." return LineReader(self.filein) def getwriter(self): "Get the resulting writer." return LineWriter(self.fileout) def readdir(self, filename, diroption): "Read the current directory if needed" if getattr(Options, diroption) != None: return setattr(Options, diroption, os.path.dirname(filename)) if getattr(Options, diroption) == '': setattr(Options, diroption, '.') class NullWriter(object): "A writer that goes nowhere." def write(self, list): "Do nothing." pass class ConverterFactory(object): "Create a converter fit for converting a filename and embedding the result." def create(self, container): "Create a converter for a given container, with filename" " and possibly other parameters." fullname = os.path.join(Options.directory, container.filename) reader = LineReader(fullname) if 'firstline' in container.parameters: reader.setstart(int(container.parameters['firstline'])) if 'lastline' in container.parameters: reader.setend(int(container.parameters['lastline'])) return eLyXerConverter().embed(reader) IncludeInset.converterfactory = ConverterFactory() def convertdoc(args): "Read a whole document and write it" Options().parseoptions(args) ioparser = InOutParser().parse(args) converter = eLyXerConverter().setio(ioparser) converter.convert() def main(): "Main function, called if invoked from the command line" convertdoc(list(sys.argv)) if __name__ == '__main__': main()