',
}
class TOCConfig(object):
"Configuration class from config file"
extractplain = {
u'allowed':[u'StringContainer',u'Constant',u'TaggedText',u'Align',u'TextFamily',u'EmphaticText',u'VersalitasText',u'BarredText',u'SizeText',u'ColorText',u'LangLine',u'Formula',],
u'cloned':[u'',], u'extracted':[u'',],
}
extracttitle = {
u'allowed':[u'StringContainer',u'Constant',u'Space',],
u'cloned':[u'TextFamily',u'EmphaticText',u'VersalitasText',u'BarredText',u'SizeText',u'ColorText',u'LangLine',u'Formula',],
u'extracted':[u'PlainLayout',u'TaggedText',u'Align',u'Caption',u'StandardLayout',],
}
class TagConfig(object):
"Configuration class from config file"
barred = {
u'under':u'u',
}
family = {
u'sans':u'span class="sans"', u'typewriter':u'tt',
}
flex = {
u'CharStyle:Code':u'span class="code"',
u'CharStyle:MenuItem':u'span class="menuitem"',
}
group = {
u'layouts':[u'Quotation',u'Quote',],
}
layouts = {
u'Center':u'div', u'Chapter':u'h?', u'Date':u'h2', 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'float-algorithm':u'Algorithm ', u'float-figure':u'Figure ',
u'float-listing':u'Listing ', u'float-table':u'Table ',
u'float-tableau':u'Tableau ', u'generated-by':u'Document generated by ',
u'generated-on':u' on ', u'index':u'Index',
u'jsmath-enable':u'Please enable JavaScript on your browser.',
u'jsmath-requires':u' requires JavaScript to correctly process the mathematics on this page. ',
u'jsmath-warning':u'Warning: ', u'list-algorithm':u'List of Algorithms',
u'list-figure':u'List of Figures', u'list-table':u'List of Tables',
u'list-tableau':u'List of Tableaux', u'main-page':u'Main page',
u'next':u'Next', u'nomenclature':u'Nomenclature',
u'on-page':u' on page ', u'prev':u'Previous',
u'toc':u'Table of Contents', u'toc-for':u'Contents for ', u'up':u'Up',
}
languages = {
u'american':u'en', u'british':u'en', u'deutsch':u'de', u'dutch':u'nl',
u'english':u'en', u'french':u'fr', u'ngerman':u'de', u'spanish':u'es',
}
import gettext
class DocumentParameters(object):
"Global parameters for the document."
pdftitle = None
indentstandard = False
tocdepth = 10
startinglevel = 0
maxdepth = 10
language = None
class Translator(object):
"Reads the configuration file and tries to find a translation."
"Otherwise falls back to the messages in the config file."
instance = None
def translate(cls, key):
"Get the translated message for a key."
return cls.instance.getmessage(key)
translate = classmethod(translate)
def __init__(self):
self.translation = None
self.first = True
def findtranslation(self):
"Find the translation for the document language."
self.langcodes = None
if not DocumentParameters.language:
Trace.error('No language in document')
return
if not DocumentParameters.language in TranslationConfig.languages:
Trace.error('Unknown language ' + DocumentParameters.language)
return
if TranslationConfig.languages[DocumentParameters.language] == 'en':
return
langcodes = [TranslationConfig.languages[DocumentParameters.language]]
try:
self.translation = gettext.translation('elyxer', None, langcodes)
except IOError:
Trace.error('No translation for ' + unicode(langcodes))
def getmessage(self, key):
"Get the translated message for the given key."
if self.first:
self.findtranslation()
self.first = False
message = self.getuntranslated(key)
if not self.translation:
return message
try:
message = self.translation.ugettext(message)
except IOError:
pass
return message
def getuntranslated(self, key):
"Get the untranslated message."
if not key in TranslationConfig.constants:
Trace.error('Cannot translate ' + key)
return key
return TranslationConfig.constants[key]
Translator.instance = Translator()
class TranslationExport(object):
"Export the translation to a file."
def __init__(self, writer):
self.writer = writer
def export(self, constants):
"Export the translation constants as a .po file."
self.writer.writeline('# SOME DESCRIPTIVE TITLE.')
self.writer.writeline('# eLyXer version ' + GeneralConfig.version['number'])
self.writer.writeline('# Released on ' + GeneralConfig.version['date'])
self.writer.writeline(u'# Contact: Alex Fernández ')
self.writer.writeline('# This file is distributed under the same license as the eLyXer package.')
self.writer.writeline('# (C) YEAR FIRST AUTHOR .')
self.writer.writeline('#')
self.writer.writeline('#, fuzzy')
self.writer.writeline('msgid ""')
self.writer.writeline('msgstr ""')
self.writer.writeline('')
for key, message in constants.iteritems():
self.writer.writeline('')
self.writer.writeline('#: ' + key)
self.writer.writeline('msgid "' + message + '"')
self.writer.writeline('msgstr "' + message + '"')
self.writer.close()
class NumberGenerator(object):
"A number generator for unique sequences and hierarchical structures"
letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
instance = None
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, level):
"Generate ordered numbering: a number to use and possibly concatenate "
"with others. Example: Chapter 1, Section 1.5."
if level == 0:
Trace.error('Impossible level 0 for ordered part')
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, chapter = None):
"Generate a number which goes with first-level numbers (chapters). "
"For the article classes a unique number is generated."
if DocumentParameters.startinglevel > 0:
return self.generateunique(type)
if not chapter:
chapter = self.getchapter()
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 getchapter(self):
"Get the current chapter number."
if len(self.number) == 0:
return 0
else:
return self.number[0]
def increase(self, number):
"Increase the number (or letter)"
if not isinstance(number, str):
return number + 1
if number == '-':
index = 0
elif not number in NumberGenerator.letters:
Trace.error('Unknown letter numeration ' + number)
return 0
else:
index = NumberGenerator.letters.index(number) + 1
return self.letter(index)
def letter(self, index):
"Get the letter that corresponds to the given index."
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:]
NumberGenerator.instance = NumberGenerator()
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"
if not self.ending:
Trace.error('No ending for ' + unicode(self))
return
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, container):
Parser.__init__(self)
self.ending = None
if container.__class__.__name__ in ContainerConfig.endings:
self.ending = ContainerConfig.endings[container.__class__.__name__]
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 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."
tag = None
breaklines = False
empty = False
def settag(self, tag, breaklines=False, empty=False):
"Set the value for the tag and other attributes."
self.tag = tag
if breaklines:
self.breaklines = breaklines
if empty:
self.empty = empty
return self
def setbreaklines(self, breaklines):
"Set the value for breaklines."
self.breaklines = breaklines
return self
def gethtml(self, container):
"Return the HTML code."
if self.empty:
return [self.selfclosing(container)]
html = [self.open(container)]
html += ContentsOutput.gethtml(self, container)
html.append(self.close(container))
return html
def open(self, container):
"Get opening line."
if not self.checktag():
return ''
open = '<' + self.tag + '>'
if self.breaklines:
return open + '\n'
return open
def close(self, container):
"Get closing line."
if not self.checktag():
return ''
close = '' + self.tag.split()[0] + '>'
if self.breaklines:
return '\n' + close + '\n'
return close
def selfclosing(self, container):
"Get self-closing line."
if not self.checktag():
return ''
selfclosing = '<' + self.tag + '/>'
if self.breaklines:
return selfclosing + '\n'
return selfclosing
def checktag(self):
"Check that the tag is valid."
if not self.tag:
Trace.error('No tag in ' + unicode(container))
return False
if self.tag == '':
return False
return True
class StringOutput(object):
"Returns a bare string as output"
def gethtml(self, container):
"Return a bare string"
return [container.string]
class Cloner(object):
"An object used to clone other objects."
def clone(cls, original):
"Return an exact copy of an object."
"The original object must have an empty constructor."
return cls.create(original.__class__)
def create(cls, type):
"Create an object of a given class."
clone = type.__new__(type)
clone.__init__()
return clone
clone = classmethod(clone)
create = classmethod(create)
class ContainerExtractor(object):
"A class to extract certain containers."
def __init__(self, config):
"The config parameter is a map containing three lists: allowed, copied and extracted."
"Each of the three is a list of class names for containers."
"Allowed containers are included as is into the result."
"Cloned containers are cloned and placed into the result."
"Extracted containers are looked into."
"All other containers are silently ignored."
self.allowed = config['allowed']
self.cloned = config['cloned']
self.extracted = config['extracted']
def extract(self, container):
"Extract a group of selected containers from a container."
list = []
locate = lambda c: c.__class__.__name__ in self.allowed + self.cloned
recursive = lambda c: c.__class__.__name__ in self.extracted
process = lambda c: self.process(c, list)
container.recursivesearch(locate, recursive, process)
return list
def process(self, container, list):
"Add allowed containers, clone cloned containers and add the clone."
name = container.__class__.__name__
if name in self.allowed:
list.append(container)
elif name in self.cloned:
list.append(self.safeclone(container))
else:
Trace.error('Unknown container class ' + name)
def safeclone(self, container):
"Return a new container with contents only in a safe list, recursively."
clone = Cloner.clone(container)
clone.output = container.output
clone.contents = self.extract(container)
return clone
class Position(object):
"A position in a text to parse"
def __init__(self):
self.endinglist = EndingList()
def checkbytemark(self):
"Check for a Unicode byte mark and skip it."
if self.finished():
return
if ord(self.current()) == 0xfeff:
self.currentskip()
def skip(self, string):
"Skip a string"
Trace.error('Unimplemented skip()')
def identifier(self):
"Return an identifier for the current position."
Trace.error('Unimplemented identifier()')
return 'Error'
def isout(self):
"Find out if we are out of the position yet."
Trace.error('Unimplemented isout()')
return True
def current(self):
"Return the current character"
Trace.error('Unimplemented current()')
return ''
def checkfor(self, string):
"Check for a string at the given position."
Trace.error('Unimplemented checkfor()')
return False
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 currentskip(self):
"Return the current character and skip it."
current = self.current()
self.skip(current)
return current
def next(self):
"Advance the position and return the next character."
self.currentskip()
return self.current()
def checkskip(self, string):
"Check for a string at the given position; if there, skip it"
if not self.checkfor(string):
return False
self.skip(string)
return True
def 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 globnumber(self):
"Glob a row of digits."
return self.glob(lambda current: current.isdigit())
def checkidentifier(self):
"Check if the current character belongs to an identifier."
return self.isidentifier(self.current())
def isidentifier(self, char):
"Return if the given character is alphanumeric or _."
if char.isalnum() or char == '_':
return True
return False
def globidentifier(self):
"Glob alphanumeric and _ symbols."
return self.glob(lambda current: self.isidentifier(current))
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 TextPosition(Position):
"A parse position based on a raw text."
def __init__(self, text):
"Create the position from some text."
Position.__init__(self)
self.pos = 0
self.text = text
self.checkbytemark()
def skip(self, string):
"Skip a string of characters."
self.pos += len(string)
def identifier(self):
"Return a sample of the remaining text."
length = 30
if self.pos + length > len(self.text):
length = len(self.text) - self.pos - 1
return '*' + self.text[self.pos:self.pos + length]
def isout(self):
"Find out if we are out of the text yet."
return self.pos >= len(self.text)
def current(self):
"Return the current character, assuming we are not out."
return self.text[self.pos]
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
class FilePosition(Position):
"A parse position based on an underlying file."
def __init__(self, filename):
"Create the position from a file."
Position.__init__(self)
self.reader = LineReader(filename)
self.number = 1
self.pos = 0
self.checkbytemark()
def skip(self, string):
"Skip a string of characters."
length = len(string)
while self.pos + length > len(self.reader.currentline()):
length -= len(self.reader.currentline()) - self.pos + 1
self.nextline()
self.pos += length
def nextline(self):
"Go to the next line."
self.reader.nextline()
self.number += 1
self.pos = 0
def identifier(self):
"Return the current line and line number in the file."
before = self.reader.currentline()[:self.pos - 1]
after = self.reader.currentline()[self.pos:]
return 'line ' + unicode(self.number) + ': ' + before + '*' + after
def isout(self):
"Find out if we are out of the text yet."
if self.pos > len(self.reader.currentline()):
if self.pos > len(self.reader.currentline()) + 1:
Trace.error('Out of the line ' + self.reader.currentline() + ': ' + unicode(self.pos))
self.nextline()
return self.reader.finished()
def current(self):
"Return the current character, assuming we are not out."
if self.pos == len(self.reader.currentline()):
return '\n'
if self.pos > len(self.reader.currentline()):
Trace.error('Out of the line ' + self.reader.currentline() + ': ' + unicode(self.pos))
return '*'
return self.reader.currentline()[self.pos]
def checkfor(self, string):
"Check for a string at the given position."
if self.pos + len(string) > len(self.reader.currentline()):
return False
return self.reader.currentline()[self.pos : self.pos + len(string)] == string
class EndingList(object):
"A list of position endings"
def __init__(self):
self.endings = []
def add(self, ending, optional):
"Add a new ending to the list"
self.endings.append(PositionEnding(ending, optional))
def checkin(self, pos):
"Search for an ending"
if self.findending(pos):
return True
return False
def pop(self, pos):
"Remove the ending at the current position"
if pos.isout():
Trace.error('No ending out of bounds')
return ''
ending = self.findending(pos)
if not ending:
Trace.error('No ending at ' + pos.current())
return ''
for each in reversed(self.endings):
self.endings.remove(each)
if each == ending:
return each.ending
elif not each.optional:
Trace.error('Removed non-optional ending ' + each)
Trace.error('No endings left')
return ''
def findending(self, pos):
"Find the ending at the current position"
if len(self.endings) == 0:
return None
for index, ending in enumerate(reversed(self.endings)):
if ending.checkin(pos):
return ending
if not ending.optional:
return None
return None
def checkpending(self):
"Check if there are any pending endings"
if len(self.endings) != 0:
Trace.error('Pending ' + unicode(self) + ' left open')
def __unicode__(self):
"Printable representation"
string = 'endings ['
for ending in self.endings:
string += unicode(ending) + ','
if len(self.endings) > 0:
string = string[:-1]
return string + ']'
class PositionEnding(object):
"An ending for a parsing position"
def __init__(self, ending, optional):
self.ending = ending
self.optional = optional
def checkin(self, pos):
"Check for the ending"
return pos.checkfor(self.ending)
def __unicode__(self):
"Printable representation"
string = 'Ending ' + self.ending
if self.optional:
string += ' (optional)'
return string
class Container(object):
"A container for text and objects in a lyx file"
partkey = None
parent = None
begin = None
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]
return self.escapeall(html)
def escapeall(self, lines):
"Escape all lines in an array according to the output options."
result = []
for line in lines:
if Options.html:
line = self.escape(line, EscapeConfig.html)
if Options.iso885915:
line = self.escape(line, EscapeConfig.iso885915)
line = self.escapeentities(line)
elif not Options.unicode:
line = self.escape(line, EscapeConfig.nonunicode)
result.append(line)
return result
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 escapeentities(self, line):
"Escape all Unicode characters to HTML entities."
result = ''
pos = TextPosition(line)
while not pos.finished():
if ord(pos.current()) > 128:
codepoint = hex(ord(pos.current()))
if codepoint == '0xd835':
codepoint = hex(ord(pos.next()) + 0xf800)
result += '' + codepoint[1:] + ';'
else:
result += pos.current()
pos.currentskip()
return result
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.searchall(type)
for container in list:
container.parent.contents.remove(container)
return list
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 recursivesearch(self, locate, recursive, process):
"Perform a recursive search in the container."
for container in self.contents:
if recursive(container):
container.recursivesearch(locate, recursive, process)
if locate(container):
process(container)
def extracttext(self):
"Extract all text from allowed containers."
result = ''
constants = ContainerExtractor(ContainerConfig.extracttext).extract(self)
for constant in constants:
result += constant.string
return result
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 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 self.begin:
return self.__class__.__name__
return self.__class__.__name__ + '@' + unicode(self.begin)
def getparameter(self, name):
"Get the value of a parameter, if present."
if not name in self.parameters:
return None
return self.parameters[name]
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 old format version ' + str(version))
if version > int(GeneralConfig.version['lyxformat']):
Trace.error('Warning: unsupported new format version ' + str(version))
class StringContainer(Container):
"A container for a single string"
parsed = None
def __init__(self):
self.parser = StringParser()
self.output = StringOutput()
self.string = ''
def process(self):
"Replace special chars from the contents."
if self.parsed:
self.string = self.replacespecial(self.parsed)
self.parsed = None
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
if self.begin:
message = 'Unknown command at ' + unicode(self.begin) + ': '
else:
message = 'Unknown command: '
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):
"Return a printable representation."
result = 'StringContainer'
if self.begin:
result += '@' + unicode(self.begin)
ellipsis = '...'
if len(self.string.strip()) <= 15:
ellipsis = ''
return result + ' (' + self.string.strip()[:15] + ellipsis + ')'
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"
output = None
def __init__(self):
self.parser = TextParser(self)
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 a printable representation."
if not hasattr(self.output, 'tag'):
return 'Emtpy tagged text'
if not self.output.tag:
return 'Tagged '
return 'Tagged <' + self.output.tag + '>'
class ContainerSize(object):
"The size of a container."
width = None
height = None
maxwidth = None
maxheight = None
scale = None
def set(self, width = None, height = None):
"Set the proper size with width and height."
self.setvalue('width', width)
self.setvalue('height', height)
return self
def setmax(self, maxwidth = None, maxheight = None):
"Set max width and/or height."
self.setvalue('maxwidth', maxwidth)
self.setvalue('maxheight', maxheight)
return self
def readparameters(self, container):
"Read some size parameters off a container."
self.setparameter(container, 'width')
self.setparameter(container, 'height')
self.setparameter(container, 'scale')
self.checkvalidheight(container)
return self
def setparameter(self, container, name):
"Read a size parameter off a container, and set it if present."
value = container.getparameter(name)
self.setvalue(name, value)
def setvalue(self, name, value):
"Set the value of a parameter name, only if it's valid."
value = self.processparameter(value)
if value:
setattr(self, name, value)
def checkvalidheight(self, container):
"Check if the height parameter is valid; otherwise erase it."
heightspecial = container.getparameter('height_special')
if self.height and self.extractnumber(self.height) == '1' and heightspecial == 'totalheight':
self.height = None
def processparameter(self, value):
"Do the full processing on a parameter."
if not value:
return None
if self.extractnumber(value) == '0':
return None
for ignored in StyleConfig.size['ignoredtexts']:
if ignored in value:
value = value.replace(ignored, '')
return value
def extractnumber(self, text):
"Extract the first number in the given text."
result = ''
decimal = False
for char in text:
if char.isdigit():
result += char
elif char == '.' and not decimal:
result += char
decimal = True
else:
return result
return result
def checkimage(self, width, height):
"Check image dimensions, set them if possible."
if width:
self.maxwidth = unicode(width) + 'px'
if self.scale and not self.width:
self.width = self.scalevalue(width)
if height:
self.maxheight = unicode(height) + 'px'
if self.scale and not self.height:
self.height = self.scalevalue(height)
if self.width and not self.height:
self.height = 'auto'
if self.height and not self.width:
self.width = 'auto'
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'
def removepercentwidth(self):
"Remove percent width if present, to set it at the figure level."
if not self.width:
return None
if not '%' in self.width:
return None
width = self.width
self.width = None
if self.height == 'auto':
self.height = None
return width
def addstyle(self, container):
"Add the proper style attribute to the output tag."
if not isinstance(container.output, TaggedOutput):
Trace.error('No tag to add style, in ' + unicode(container))
if not self.width and not self.height and not self.maxwidth and not self.maxheight:
# nothing to see here; move along
return
tag = ' style="'
tag += self.styleparameter('width')
tag += self.styleparameter('maxwidth')
tag += self.styleparameter('height')
tag += self.styleparameter('maxheight')
if tag[-1] == ' ':
tag = tag[:-1]
tag += '"'
container.output.tag += tag
def styleparameter(self, name):
"Get the style for a single parameter."
value = getattr(self, name)
if value:
return name.replace('max', 'max-') + ': ' + value + '; '
return ''
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):
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(BlackBox):
"A line with language information"
def process(self):
self.lang = self.header[1]
class InsetLength(BlackBox):
"A length measure inside an inset."
def process(self):
self.length = 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.hspaces:
Trace.error('Unknown space type ' + self.type)
self.html = [' ']
return
self.html = [StyleConfig.hspaces[self.type]]
length = self.getlength()
if not length:
return
self.output = TaggedOutput().settag('span class="hspace"', False)
ContainerSize().set(length).addstyle(self)
def getlength(self):
"Get the space length from the contents or parameters."
if len(self.contents) == 0 or not isinstance(self.contents[0], InsetLength):
return None
return self.contents[0].length
class VerticalSpace(Container):
"An inset that contains a vertical space."
def __init__(self):
self.parser = InsetParser()
self.output = FixedOutput()
def process(self):
"Set the correct tag"
self.type = self.header[2]
if self.type not in StyleConfig.vspaces:
self.output = TaggedOutput().settag('div class="vspace" style="height: ' + self.type + ';"', True)
return
self.html = [StyleConfig.vspaces[self.type]]
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(BlackBox):
"An appendix to the main document"
def process(self):
"Activate the special numbering scheme for appendices, using letters."
NumberGenerator.instance.number = ['-']
class ERT(Container):
"Evil Red Text"
def __init__(self):
self.parser = InsetParser()
self.output = EmptyOutput()
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
self.title = None
if Options.target:
self.target = Options.target
def complete(self, text, anchor = None, url = None, type = None, title = None):
"Complete the link."
self.contents = [Constant(text)]
if anchor:
self.anchor = anchor
if url:
self.url = url
if type:
self.type = type
if title:
self.title = title
return self
def computedestination(self):
"Use the destination link to fill in the destination URL."
if not self.destination:
return
self.url = ''
if self.destination.anchor:
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 URL(Link):
"A clickable URL"
def process(self):
"Read URL from parameters"
target = self.escape(self.getparameter('target'))
self.url = target
type = self.getparameter('type')
if type:
self.url = self.escape(type) + target
name = self.getparameter('name')
if not name:
name = target
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 + '"'
if link.title:
tag += ' title="' + link.title + '"'
return TaggedOutput().settag(tag).gethtml(link)
class Postprocessor(object):
"Postprocess a container keeping some context"
stages = []
def __init__(self):
self.stages = StageDict(Postprocessor.stages, self)
self.current = None
self.last = None
def postprocess(self, next):
"Postprocess a 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
if hasattr(container, 'postprocess'):
if not container.postprocess:
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 Label(Link):
"A label to be referenced"
names = dict()
lastlayout = None
def __init__(self):
Link.__init__(self)
self.lastnumbered = None
def process(self):
"Process a label container."
key = self.getparameter('name')
self.create(' ', key)
self.lastnumbered = Label.lastlayout
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 labelnumber(self):
"Get the number for the latest numbered container seen."
numbered = self.numbered(self)
if numbered and numbered.partkey and numbered.partkey.number:
return numbered.partkey.number
return ''
def numbered(self, container):
"Get the numbered container for the label."
if container.partkey:
return container
if not container.parent:
if self.lastnumbered:
return self.lastnumbered
return None
return self.numbered(container.parent)
def __unicode__(self):
"Return a printable representation."
if not hasattr(self, 'key'):
return 'Unnamed label'
return 'Label ' + self.key
class Reference(Link):
"A reference to a label."
references = dict()
formats = {
'ref':u'@↕', 'eqref':u'(@↕)', 'pageref':u'#↕',
'vref':u'@on-page#↕'
}
key = 'none'
def process(self):
"Read the reference and set the arrow."
self.key = self.getparameter('reference')
if self.key in Label.names:
self.direction = u'↑'
label = Label.names[self.key]
else:
self.direction = u'↓'
label = Label().complete(' ', self.key, 'preref')
self.destination = label
self.format()
if not self.key in Reference.references:
Reference.references[self.key] = []
Reference.references[self.key].append(self)
def format(self):
"Format the reference contents."
formatkey = self.getparameter('LatexCommand')
if not formatkey:
formatkey = 'ref'
if not formatkey in self.formats:
Trace.error('Unknown reference format ' + formatkey)
formatstring = u'↕'
else:
formatstring = self.formats[formatkey]
formatstring = formatstring.replace(u'↕', self.direction)
formatstring = formatstring.replace('@', self.destination.labelnumber())
formatstring = formatstring.replace('#', '1')
formatstring = formatstring.replace('on-page', Translator.translate('on-page'))
self.contents = [Constant(formatstring)]
def __unicode__(self):
"Return a printable representation."
return 'Reference ' + self.key
class HeaderParser(Parser):
"Parses the LyX header"
def parse(self, reader):
"Parse header parameters into a dictionary, return the preamble."
contents = []
self.parseending(reader, lambda: self.parseline(reader, contents))
# skip last line
reader.nextline()
return contents
def parseline(self, reader, contents):
"Parse a single line as a parameter or as a start"
line = reader.currentline()
if line.startswith(HeaderConfig.parameters['branch']):
self.parsebranch(reader)
return
elif line.startswith(HeaderConfig.parameters['lstset']):
LstParser().parselstset(reader)
return
elif line.startswith(HeaderConfig.parameters['beginpreamble']):
contents.append(self.factory.createcontainer(reader))
return
# no match
self.parseparameter(reader)
def parsebranch(self, reader):
"Parse all branch definitions."
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):
"Complete the parser with the given ending."
self.ending = ending
return self
class PreambleParser(Parser):
"A parser for the LyX preamble."
preamble = []
def parse(self, reader):
"Parse the full preamble with all statements."
self.ending = HeaderConfig.parameters['endpreamble']
self.parseending(reader, lambda: self.parsepreambleline(reader))
return []
def parsepreambleline(self, reader):
"Parse a single preamble line."
PreambleParser.preamble.append(reader.currentline())
reader.nextline()
class LstParser(object):
"Parse global and local lstparams."
globalparams = dict()
def parselstset(self, reader):
"Parse a declaration of lstparams in lstset."
paramtext = self.extractlstset(reader)
if not '{' in paramtext:
Trace.error('Missing opening bracket in lstset: ' + paramtext)
return
lefttext = paramtext.split('{')[1]
croppedtext = lefttext[:-1]
LstParser.globalparams = self.parselstparams(croppedtext)
def extractlstset(self, reader):
"Extract the global lstset parameters."
paramtext = ''
while not reader.finished():
paramtext += reader.currentline()
reader.nextline()
if paramtext.endswith('}'):
return paramtext
Trace.error('Could not find end of \\lstset settings; aborting')
def parsecontainer(self, container):
"Parse some lstparams from a container."
container.lstparams = LstParser.globalparams.copy()
if not 'lstparams' in container.parameters:
return
paramtext = container.parameters['lstparams']
container.lstparams.update(self.parselstparams(paramtext))
def parselstparams(self, text):
"Parse a number of lstparams from a text."
paramdict = dict()
paramlist = text.split(',')
for param in paramlist:
if not '=' in param:
if len(param.strip()) > 0:
Trace.error('Invalid listing parameter ' + param)
else:
key, value = param.split('=', 1)
paramdict[key] = value
return paramdict
import datetime
import os
import codecs
class BulkFile(object):
"A file to treat in bulk"
encodings = ['utf-8','Cp1252']
def __init__(self, filename):
self.filename = filename
self.temp = self.filename + '.temp'
def readall(self):
"Read the whole file"
for encoding in BulkFile.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, 'rU', encoding)
lines = filein.readlines()
result = []
for line in lines:
result.append(line.strip('\r\n') + '\n')
filein.close()
return result
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 HTMLTemplate(object):
"A template for HTML generation."
current = None
def getheader(self):
"Get the header (before content) of the template."
return []
def convertheader(self):
"Convert the header and all variables."
return self.convert(self.getheader())
def convertfooter(self):
"Convert the footer and all variables."
return self.convert(self.getfooter())
def convert(self, html):
"Convert a bit of HTML replacing all variables."
varmap = VariableMap()
for index, line in enumerate(html):
if '\n']
def getfooter(self):
"Get the raw footer."
return ['\n\n']
class FileTemplate(HTMLTemplate):
"A template read from a file."
divider = ''
def read(self):
"Read the file, separate header and footer."
self.header = []
lines = []
for line in self.templatelines():
if FileTemplate.divider == line:
self.header = lines
lines = []
else:
lines.append(line)
if self.header == []:
Trace.error('No ' + FileTemplate.divider + ' in template')
self.header = lines
lines = []
self.footer = lines
return self
def templatelines(self):
"Read all lines in the template, separate content into its own line."
template = BulkFile(Options.template).readall()
for line in template:
if not FileTemplate.divider in line:
yield line
else:
split = line.split(FileTemplate.divider)
for part in split[:-1]:
yield part
yield FileTemplate.divider
yield split[-1]
def getheader(self):
"Return the header (before content)."
return self.header
def getfooter(self):
"Return the footer (after the content)."
return self.footer
class DefaultTemplate(HTMLTemplate):
"The default HTML template when not configured."
def getheader(self):
"Get the default header (before content)."
html = []
if not Options.html:
html.append(u'"?>\n')
html.append(u'\n')
html.append(u'\n')
else:
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.append(u'\n')
html.append(u'\n')
if Options.jsmath:
html.append(u'\n')
html.append(u'\n')
if Options.mathjax:
html.append(u'\n')
html.append('\n')
html.append('\n')
html.append('
\n')
if Options.jsmath or Options.mathjax:
if Options.mathjax:
html.append(u'\n')
html.append(u'\n')
return html
def getfooter(self):
"Get the default footer (after content)."
html = []
html.append('\n')
footer = self.createfooter()
if len(footer) > 0:
html.append('\n')
html += footer
html.append('
\n')
html.append('\n')
html.append('\n')
return html
def createfooter(self):
"Create the footer proper."
html = []
if Options.copyright:
html.append('\n')
if Options.nofooter:
return html
html.append('\n')
return html
class VariableMap(object):
"A map with all replacement variables."
def __init__(self):
self.variables = dict()
self.variables['title'] = DocumentTitle().getvalue()
self.variables['author'] = DocumentAuthor().getvalue()
self.variables['version'] = GeneralConfig.version['number'] + ' (' \
+ GeneralConfig.version['date'] + ')'
self.variables['year'] = unicode(datetime.date.today().year)
self.variables['date'] = datetime.date.today().isoformat()
self.variables['datetime'] = datetime.datetime.now().isoformat()
self.variables['css'] = Options.css
if Options.iso885915:
self.variables['encoding'] = 'ISO-8859-1'
else:
self.variables['encoding'] = 'UTF-8'
if Options.jsmath:
self.variables['jsmath'] = Options.jsmath
if Options.mathjax:
self.variables['mathjax'] = Options.mathjax
def replace(self, line):
"Replace all variables in a line."
result = ''
pos = TextPosition(line)
while not pos.finished():
if pos.checkskip(''):
Trace.error('Weird template format in ' + line)
return value
class DocumentTitle(object):
"The title of the whole document."
title = None
def getvalue(self):
"Return the correct title from the option or the PDF title."
if Options.title:
return Options.title
if DocumentTitle.title:
return DocumentTitle.title
if DocumentParameters.pdftitle:
return DocumentParameters.pdftitle
return 'Converted document'
class DocumentAuthor(object):
"The author of the document."
author = ''
def appendauthor(cls, authorline):
"Append a line with author information."
cls.author += authorline
appendauthor = classmethod(appendauthor)
def getvalue(self):
"Get the document author."
return DocumentAuthor.author
class HeaderOutput(object):
"Returns the HTML headers"
def gethtml(self, container):
"Return a constant header"
return HTMLTemplate.get().convertheader()
class FooterOutput(object):
"Return the HTML code for the footer"
def gethtml(self, container):
"Footer HTML"
return HTMLTemplate.get().convertfooter()
class PartKey(object):
"A key to identify a given document part (chapter, section...)."
partkey = None
tocentry = None
tocsuffix = None
anchortext = None
number = None
filename = None
header = False
def __init__(self):
self.level = 0
def createindex(self, partkey):
"Create a part key for an index page."
self.partkey = partkey
self.tocentry = partkey
self.filename = partkey
return self
def createfloat(self, partkey, number):
"Create a part key for a float."
self.partkey = partkey
self.number = number
self.tocentry = partkey
self.tocsuffix = u':'
return self
def createformula(self, number):
"Create the part key for a formula."
self.number = number
self.partkey = 'formula-' + number
self.tocentry = '(' + number + ')'
return self
def createheader(self, headorfooter):
"Create the part key for a header or footer."
self.partkey = headorfooter
self.tocentry = None
self.header = True
return self
def createanchor(self, partkey):
"Create an anchor for the page."
self.partkey = partkey
self.tocentry = partkey
self.header = True
return self
def toclabel(self):
"Create the label for the TOC."
labeltext = ''
if self.anchortext:
labeltext = self.anchortext
return Label().create(labeltext, self.partkey, type='toc')
def __unicode__(self):
"Return a printable representation."
return 'Part key for ' + self.partkey
class LayoutPartKey(PartKey):
"The part key for a layout."
generator = NumberGenerator.instance
unique = NumberingConfig.layouts['unique']
ordered = NumberingConfig.layouts['ordered']
def __init__(self, layout):
"Create a part key for a layout."
self.level = self.getlevel(layout.type)
if LayoutPartKey.isunique(layout):
self.number = self.generator.generateunique(layout.type)
else:
if self.isnumbered(layout):
self.number = self.generator.generateordered(self.level)
else:
self.number = self.generator.generateunique(layout.type)
anchortype = layout.type.replace('*', '-')
self.partkey = 'toc-' + anchortype + '-' + self.number
realtype = self.deasterisk(layout.type)
self.tocentry = Translator.translate(realtype)
self.tocsuffix = u': '
if self.level == Options.splitpart and self.isnumbered(layout):
self.filename = self.number
elif self.level <= Options.splitpart:
self.filename = self.partkey.replace('toc-', '').replace('*', '-')
if self.isnumbered(layout):
self.tocentry += ' ' + self.number
self.tocsuffix = u':'
if self.isunique(layout):
self.anchortext = self.tocentry + '.'
else:
self.anchortext = self.number
def getlevel(self, type):
"Get the level that corresponds to a type."
type = self.deasterisk(type)
if type in self.unique:
return 0
level = self.ordered.index(type) + 1
return level - DocumentParameters.startinglevel
def isnumbered(self, layout):
"Find out if a layout is numbered."
if '*' in layout.type:
return False
if self.getlevel(layout.type) > DocumentParameters.maxdepth:
return False
return True
def isunique(cls, layout):
"Find out if the layout requires unique numbering."
return cls.deasterisk(layout.type) in LayoutPartKey.unique
def isinordered(cls, layout):
"Find out if a layout is ordered or unordered."
return cls.deasterisk(layout.type) in LayoutPartKey.ordered
def needspartkey(cls, layout):
"Find out if a layout needs a part key."
if cls.isunique(layout):
return True
return cls.isinordered(layout)
def deasterisk(cls, type):
"Get the type without the asterisk for unordered types."
return type.replace('*', '')
def __unicode__(self):
"Get a printable representation."
return 'Part key for layout ' + self.tocentry
isunique = classmethod(isunique)
isinordered = classmethod(isinordered)
needspartkey = classmethod(needspartkey)
deasterisk = classmethod(deasterisk)
class PartKeyGenerator(object):
"Number a layout with the relevant attributes."
partkeyed = []
def forlayout(cls, layout):
"Get the part key for a layout."
if not LayoutPartKey.needspartkey(layout):
return None
Label.lastlayout = layout
cls.partkeyed.append(layout)
return LayoutPartKey(layout)
def forindex(cls, index):
"Get the part key for an index or nomenclature."
cls.partkeyed.append(index)
return PartKey().createindex(index.name)
forlayout = classmethod(forlayout)
forindex = classmethod(forindex)
class LyXHeader(Container):
"Reads the header, outputs the HTML header"
def __init__(self):
self.contents = []
self.parser = HeaderParser()
self.output = HeaderOutput()
self.parameters = dict()
self.partkey = PartKey().createheader('header')
def process(self):
"Find pdf title"
DocumentParameters.pdftitle = self.getheaderparameter('pdftitle')
if self.getheaderparameter('documentclass') in HeaderConfig.styles['article']:
DocumentParameters.startinglevel = 1
if self.getheaderparameter('paragraphseparation') == 'indent':
DocumentParameters.indentstandard = True
DocumentParameters.tocdepth = self.getlevel('tocdepth')
DocumentParameters.maxdepth = self.getlevel('secnumdepth')
DocumentParameters.language = self.getheaderparameter('language')
return self
def getheaderparameter(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."
paramvalue = self.getheaderparameter(configparam)
if not paramvalue:
return 0
value = int(paramvalue)
if DocumentParameters.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()
self.partkey = PartKey().createheader('footer')
class Layout(Container):
"A layout (block of text) inside a lyx file"
type = 'none'
def __init__(self):
"Initialize the layout."
self.contents = []
self.parser = BoundedParser()
self.output = TaggedOutput().setbreaklines(True)
def process(self):
"Get the type and numerate if necessary."
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('*', '')]
self.output.tag += ' class="' + self.type.replace('*', '-') + '"'
else:
self.output.tag = 'div class="' + self.type + '"'
self.numerate()
def numerate(self):
"Numerate if necessary."
partkey = PartKeyGenerator.forlayout(self)
if partkey:
self.partkey = partkey
self.output.tag = self.output.tag.replace('?', unicode(partkey.level))
def __unicode__(self):
"Return a printable representation."
if self.partkey:
return 'Layout ' + self.type + ' ' + self.partkey.number
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"'
title = self.extracttext()
DocumentTitle.title = title
Trace.message('Title: ' + title)
class Author(Layout):
"The document author"
def process(self):
self.type = 'author'
self.output.tag = 'h2 class="author"'
author = self.extracttext()
Trace.debug('Author: ' + author)
DocumentAuthor.appendauthor(author)
class Abstract(Layout):
"A paper abstract"
def process(self):
self.type = 'abstract'
self.output.tag = 'div class="abstract"'
message = Translator.translate('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):
"Extract the first word as a list"
return self.extractincontents(self.contents)
def extractincontents(self, contents):
"Extract the first word in contents."
firstcontents = []
while len(contents) > 0:
if self.spaceincontainer(contents[0]):
firstcontents += self.extractincontainer(contents[0])
return firstcontents
firstcontents.append(contents[0])
del contents[0]
return firstcontents
def extractincontainer(self, container):
"Extract all elements up to the first space."
if isinstance(container, StringContainer):
return self.extractinstring(container)
firstcontents = self.extractincontents(container.contents)
if isinstance(container, TaggedText):
return [TaggedText().complete(firstcontents, container.output.tag)]
return firstcontents
def extractinstring(self, container):
"Extract the first word from a string container."
if not ' ' in container.string:
Trace.error('No space in string ' + container.string)
return [container]
split = container.string.split(' ', 1)
container.string = split[1]
return [Constant(split[0])]
def spaceincontainer(self, container):
"Find out if the container contains a space somewhere."
if isinstance(container, StringContainer):
if self.spaceinstring(container):
return True
for element in container.contents:
if self.spaceincontainer(element):
return True
return False
def spaceinstring(self, container):
"Find out if the string container has a space."
return ' ' in container.string
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()
if not firstword:
return
tag = 'span class="Description-entry"'
self.contents.insert(0, TaggedText().complete(firstword, tag))
self.contents.insert(1, Constant(u' '))
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()
if not firstword:
return
first = TaggedText().complete(firstword, 'span class="List-entry"')
second = TaggedText().complete(self.contents, 'span class="List-contents"')
self.contents = [first, second]
class PlainLayout(Layout):
"A plain layout"
def process(self):
"Output just as contents."
self.output = ContentsOutput()
self.type = 'Plain'
def makevisible(self):
"Make the layout visible, output as tagged text."
self.output = TaggedOutput().settag('div class="PlainVisible"', True)
class LyXCode(Layout):
"A bit of LyX-Code."
def process(self):
"Output as pre."
self.output.tag = 'pre class="LyX-Code"'
for newline in self.searchall(Newline):
index = newline.parent.contents.index(newline)
newline.parent.contents[index] = Constant('\n')
class PostLayout(object):
"Numerate an indexed layout"
processedclass = Layout
def postprocess(self, last, layout, next):
"Group layouts and/or number them."
if layout.type in TagConfig.group['layouts']:
return self.group(last, layout)
if layout.partkey:
self.number(layout)
return layout
def group(self, last, layout):
"Group two layouts if they are the same type."
if not self.isgroupable(layout) or not self.isgroupable(last) or last.type != layout.type:
return layout
layout.contents = last.contents + [Constant(' \n')] + layout.contents
last.contents = []
last.output = EmptyOutput()
return layout
def isgroupable(self, container):
"Check that the container can be grouped."
if not isinstance(container, Layout):
return False
for element in container.contents:
if not element.__class__.__name__ in LayoutConfig.groupable['allowed']:
return False
return True
def number(self, layout):
"Generate a number and place it before the text"
label = layout.partkey.toclabel()
layout.contents.insert(0, label)
if layout.partkey.anchortext:
layout.contents.insert(1, Constant(u' '))
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 DocumentParameters.indentstandard:
if isinstance(last, StandardLayout):
type = 'Indented'
else:
type = 'Unindented'
standard.output = TaggedOutput().settag('div class="' + type + '"', True)
return standard
class PostPlainLayout(PostLayout):
"Numerate a plain layout"
processedclass = PlainLayout
def postprocess(self, last, plain, next):
"Group plain layouts."
if not self.istext(last) or not self.istext(plain):
return plain
plain.makevisible()
return self.group(last, plain)
def istext(self, container):
"Find out if the container is only text."
if not isinstance(container, PlainLayout):
return False
extractor = ContainerExtractor(TOCConfig.extractplain)
text = extractor.extract(container)
return (len(text) > 0)
class PostLyXCode(object):
"Coalesce contiguous LyX-Code layouts."
processedclass = LyXCode
def postprocess(self, last, lyxcode, next):
"Coalesce if last was also LyXCode"
if not isinstance(last, LyXCode):
return lyxcode
if hasattr(last, 'first'):
lyxcode.first = last.first
else:
lyxcode.first = last
toappend = lyxcode.first.contents
toappend.append(Constant('\n'))
toappend += lyxcode.contents
lyxcode.output = EmptyOutput()
return lyxcode
Postprocessor.stages += [
PostLayout, PostStandard, PostLyXCode, PostPlainLayout
]
class BiblioCitation(Container):
"A complete bibliography citation (possibly with many cites)."
citations = dict()
def __init__(self):
self.parser = InsetParser()
self.output = TaggedOutput().settag('span class="bibcites"')
self.contents = []
def process(self):
"Process the complete citation and all cites within."
self.contents = [Constant('[')]
keys = self.getparameter('key').split(',')
for key in keys:
self.contents += [BiblioCite().create(key), Constant(', ')]
if len(keys) > 0:
# remove trailing ,
self.contents.pop()
self.contents.append(Constant(']'))
class BiblioCite(Link):
"Cite of a bibliography entry"
cites = dict()
def create(self, key):
"Create the cite to the given key."
self.key = key
number = NumberGenerator.instance.generateunique('bibliocite')
ref = BiblioReference().create(key, number)
self.complete(number, 'cite-' + number, type='bibliocite')
self.setmutualdestination(ref)
if not key in BiblioCite.cites:
BiblioCite.cites[key] = []
BiblioCite.cites[key].append(self)
return self
class Bibliography(Container):
"A bibliography layout containing an entry"
def __init__(self):
self.parser = BoundedParser()
self.output = TaggedOutput().settag('p class="biblio"', True)
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) or Options.nobib:
return element
bibliography = Translator.translate('bibliography')
header = TaggedText().constant(bibliography, 'h1 class="biblio"')
layout = StandardLayout().complete([header, element])
return layout
Postprocessor.stages += [PostBiblio]
class BiblioReference(Link):
"A reference to a bibliographical entry."
references = dict()
def create(self, key, number):
"Create the reference with the given key and number."
self.key = key
self.complete(number, 'biblio-' + number, type='biblioentry')
if not key in BiblioReference.references:
BiblioReference.references[key] = []
BiblioReference.references[key].append(self)
return self
class BiblioEntry(Container):
"A bibliography entry"
entries = dict()
def __init__(self):
self.parser = InsetParser()
self.output = TaggedOutput().settag('span class="entry"')
self.contents = []
def process(self):
"Process the cites for the entry's key"
self.citeref = [Constant(NumberGenerator.instance.generateunique('biblioentry'))]
self.processcites(self.getparameter('key'))
def processcites(self, key):
"Get all the cites of the entry"
self.key = key
if not key in BiblioReference.references:
self.contents.append(Constant('[-] '))
return
self.contents = [Constant('[')]
for ref in BiblioReference.references[key]:
self.contents.append(ref)
self.contents.append(Constant(','))
self.contents.pop(-1)
self.contents.append(Constant('] '))
class Processor(object):
"Process a container and its contents."
prestages = []
skipfiltered = ['LyXHeader', 'LyXFooter', 'Title', 'Author', 'TableOfContents']
def __init__(self, filtering):
"Set filtering mode (to skip postprocessing)."
"With filtering on, the classes in skipfiltered are not processed at all."
self.filtering = filtering
self.postprocessor = Postprocessor()
def process(self, container):
"Do the whole processing on a container."
if self.filtering and container.__class__.__name__ in self.skipfiltered:
return None
container = self.preprocess(container)
self.processcontainer(container)
if not container:
# do not postprocess empty containers from here
return container
return self.postprocess(container)
def preprocess(self, root):
"Preprocess a root container with all prestages."
if not root:
return None
for stage in self.prestages:
root = stage.preprocess(root)
if not root:
return None
return root
def processcontainer(self, container):
"Process a container and its contents, recursively."
if not container:
return
for element in container.contents:
self.processcontainer(element)
container.process()
def postprocess(self, container):
"Postprocess a container, unless filtering is on."
if self.filtering:
return container
return self.postprocessor.postprocess(container)
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
sortdictionary = classmethod(sortdictionary)
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 = Translator.translate('list-' + 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"
self.create(Translator.translate('toc'))
def create(self, heading):
"Create a table of contents with the given heading text."
self.output = TaggedOutput().settag('div class="fulltoc"', True)
self.contents = [TaggedText().constant(heading, 'div class="tocheader"', True)]
return self
def add(self, entry):
"Add a new entry to the TOC."
self.contents.append(entry)
class IndexReference(Link):
"A reference to an entry in the alphabetical index."
name = 'none'
def process(self):
"Put entry in index"
name = self.getparameter('name')
if name:
self.name = name.strip()
else:
self.name = self.extracttext()
IndexEntry.get(self.name).addref(self)
def __unicode__(self):
"Return a printable representation."
return 'Reference to ' + self.name
class IndexHeader(Link):
"The header line for an index entry. Keeps all arrows."
keyescapes = {'!':'', '|':'-', ' ':'-', '--':'-', ',':'', '\\':'', '@':'_', u'°':''}
def create(self, names):
"Create the header for the given index entry."
self.output = TaggedOutput().settag('p class="printindex"', True)
self.name = names[-1]
keys = [self.escape(part, self.keyescapes) for part in names]
self.key = '-'.join(keys)
self.anchor = Link().complete('', 'index-' + self.key, None, 'printindex')
self.contents = [self.anchor, Constant(self.name + ': ')]
self.arrows = []
return self
def addref(self, reference):
"Create an arrow pointing to a reference."
reference.index = unicode(len(self.arrows))
reference.destination = self.anchor
reference.complete(u'↓', 'entry-' + self.key + '-' + reference.index)
arrow = Link().complete(u'↑', type = 'IndexArrow')
arrow.destination = reference
if len(self.arrows) > 0:
self.contents.append(Constant(u', '))
self.arrows.append(arrow)
self.contents.append(arrow)
def __unicode__(self):
"Return a printable representation."
return 'Index header for ' + self.name
class IndexGroup(Container):
"A group of entries in the alphabetical index, for an entry."
root = None
def create(self):
"Create an index group."
self.entries = dict()
self.output = EmptyOutput()
return self
def findentry(self, names):
"Find the entry with the given names."
if self == IndexGroup.root:
self.output = ContentsOutput()
else:
self.output = TaggedOutput().settag('div class="indexgroup"', True)
lastname = names[-1]
if not lastname in self.entries:
self.entries[lastname] = IndexEntry().create(names)
return self.entries[lastname]
def sort(self):
"Sort all entries in the group."
for key in ListInset.sortdictionary(self.entries):
entry = self.entries[key]
entry.group.sort()
self.contents.append(entry)
def __unicode__(self):
"Return a printable representation."
return 'Index group'
IndexGroup.root = IndexGroup().create()
class IndexEntry(Container):
"An entry in the alphabetical index."
"When an index entry is of the form 'part1 ! part2 ...', "
"a hierarchical structure in the form of an IndexGroup is constructed."
"An index entry contains a mandatory header, and an optional group."
def create(self, names):
"Create an index entry with the given name."
self.output = ContentsOutput()
self.header = IndexHeader().create(names)
self.group = IndexGroup().create()
self.contents = [self.header, self.group]
return self
def addref(self, reference):
"Add a reference to the entry."
self.header.addref(reference)
def get(cls, name):
"Get the index entry for the given name."
group = IndexGroup.root
parts = IndexEntry.splitname(name)
readparts = []
for part in parts:
readparts.append(part)
entry = group.findentry(readparts)
group = entry.group
return entry
def splitname(cls, name):
"Split a name in parts divided by !."
return [part.strip() for part in name.split('!')]
def __unicode__(self):
"Return a printable representation."
return 'Index entry for ' + self.name
get = classmethod(get)
splitname = classmethod(splitname)
class PrintIndex(ListInset):
"Command to print an index"
def process(self):
"Create the alphabetic index"
self.name = Translator.translate('index')
self.partkey = PartKeyGenerator.forindex(self)
self.contents = [self.partkey.toclabel(),
TaggedText().constant(self.name, 'h1 class="index"')]
IndexGroup.root.sort()
self.contents.append(IndexGroup.root)
class NomenclatureEntry(Link):
"An entry of LyX nomenclature"
entries = dict()
def process(self):
"Put entry in index"
symbol = self.getparameter('symbol')
description = self.getparameter('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):
"Create the nomenclature."
self.name = Translator.translate('nomenclature')
self.partkey = PartKeyGenerator.forindex(self)
self.contents = [self.partkey.toclabel(),
TaggedText().constant(self.name, '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 PreListInset(object):
"Preprocess any container that contains a list inset."
def preprocess(self, container):
"Preprocess a container, extract any list inset and return it."
listinsets = container.searchall(ListInset)
if len(listinsets) == 0:
return container
if len(container.contents) > 1:
return container
return listinsets[0]
Processor.prestages += [PreListInset()]
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 + '\n'
reader.nextline()
line = reader.currentline()
formula += line[:-len(ending)]
reader.nextline()
return formula
class MacroParser(FormulaParser):
"A parser for a formula macro."
def parseheader(self, reader):
"See if the formula is inlined"
self.begin = reader.linenumber + 1
return ['inline']
def parse(self, reader):
"Parse the formula until the end"
formula = self.parsemultiliner(reader, self.parent.start, self.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"
if self.header[0] != 'inline':
self.output.settag('div class="formula"', True)
if Options.jsmath:
if self.header[0] != 'inline':
self.output = TaggedOutput().settag('div class="math"')
else:
self.output = TaggedOutput().settag('span class="math"')
self.contents = [Constant(self.parsed)]
return
if Options.mathjax:
self.output.tag = 'span class="MathJax_Preview"'
tag = 'script type="math/tex'
if self.header[0] != 'inline':
tag += ';mode=display'
self.contents = [TaggedText().constant(self.parsed, tag + '"', True)]
return
whole = WholeFormula.parse(self.parsed)
self.contents = [whole]
whole.parent = self
def __unicode__(self):
"Return a printable representation."
if self.partkey and self.partkey.number:
return 'Formula (' + self.number + ')'
return 'Unnumbered formula'
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.identifier())
def clone(self):
"Return a copy of itself."
return WholeFormula.parse(self.original)
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(Constant):
"A constant string in a formula"
def __init__(self, string):
"Set the constant string"
Constant.__init__(self, string)
self.original = string
self.type = None
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.detectany(pos)
def parsebit(self, pos):
"Parse with any formula bit"
while self.factory.detectany(pos):
bit = self.factory.parseany(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' '))
def parse(cls, formula):
"Parse a whole formula and return it."
pos = TextPosition(formula)
whole = WholeFormula()
if not whole.detect(pos):
if pos.finished():
return FormulaConstant('')
Trace.error('Unknown formula at: ' + pos.identifier())
return TaggedBit().constant(formula, 'span class="unknown"')
whole.parsebit(pos)
whole.process()
return whole
parse = classmethod(parse)
class FormulaFactory(object):
"Construct bits of formula"
# bit types will be appended later
types = []
ignoredtypes = []
instances = {}
def detectany(self, pos):
"Detect if there is a next bit"
if pos.finished():
return False
for type in FormulaFactory.types:
if self.detecttype(type, pos):
return True
return False
def detecttype(self, type, pos):
"Detect a bit of a given type."
self.clearignored(pos)
if pos.finished():
return False
return self.instance(type).detect(pos)
def instance(self, type):
"Get an instance of the given type."
if not type in self.instances or not self.instances[type]:
self.instances[type] = Cloner.create(type)
return self.instances[type]
def clearignored(self, pos):
"Clear all ignored types."
while not pos.finished():
if not self.clearany(pos):
return
def clearany(self, pos):
"Cleary any ignored type."
for type in self.ignoredtypes:
if self.instance(type).detect(pos):
self.parsetype(type, pos)
return True
return False
def parseany(self, pos):
"Parse any formula bit at the current location."
for type in FormulaFactory.types:
if self.detecttype(type, pos):
return self.parsetype(type, pos)
Trace.error('Unrecognized formula at ' + pos.identifier())
return FormulaConstant(pos.currentskip())
def parsetype(self, type, pos):
"Parse the given type and return it."
bit = self.instance(type)
self.instances[type] = None
bit.factory = self
returnedbit = bit.parsebit(pos)
if returnedbit:
return returnedbit
return bit
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 Comment(FormulaBit):
"A LaTeX comment: % to the end of the line."
start = FormulaConfig.starts['comment']
def detect(self, pos):
"Detect the %."
return pos.current() == self.start
def parsebit(self, pos):
"Parse to the end of the line."
self.original += pos.globincluding('\n')
class WhiteSpace(FormulaBit):
"Some white space inside a formula."
def detect(self, pos):
"Detect the white space."
return pos.current().isspace()
def parsebit(self, pos):
"Parse all whitespace."
self.original += pos.skipspace()
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.identifier())
return None
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.identifier())
return
def innertext(self, pos):
"Parse some text inside the bracket, following textual rules."
factory = FormulaFactory()
specialchars = FormulaConfig.symbolfunctions.keys()
specialchars.append(FormulaConfig.starts['command'])
specialchars.append(Comment.start)
while not pos.finished():
if pos.current() in specialchars:
if factory.detectany(pos):
self.add(factory.parseany(pos))
pos.checkskip(' ')
else:
self.add(FormulaConstant(pos.currentskip()))
def innerliteral(self, pos):
"Parse a literal inside the bracket, which cannot generate html"
self.literal = ''
while not pos.current() == self.ending:
if pos.current() == self.start:
self.parseliteral(pos)
else:
self.literal += pos.currentskip()
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.types += [
FormulaSymbol, RawText, Number, Bracket
]
FormulaFactory.ignoredtypes += [
Comment, WhiteSpace
]
class FormulaCommand(FormulaBit):
"A LaTeX command inside a formula"
commandbits = []
start = FormulaConfig.starts['command']
preambling = False
def detect(self, pos):
"Find the current command"
return pos.checkfor(FormulaCommand.start)
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
if not self.preambling:
Trace.error('Unknown command ' + command)
self.output = TaggedOutput().settag('span class="unknown"')
self.add(FormulaConstant(command))
return None
def extractcommand(self, pos):
"Extract the command from the current position"
if not pos.checkskip(FormulaCommand.start):
Trace.error('Missing command start ' + start)
return
if pos.current().isalpha():
# alpha command
command = FormulaCommand.start + pos.globalpha()
# skip mark of short command
pos.checkskip('*')
return command
# symbol command
return FormulaCommand.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.detectany(pos):
Trace.error('No parameter found at: ' + pos.identifier())
return None
parameter = self.factory.parseany(pos)
self.add(parameter)
return parameter
def parsesquare(self, pos):
"Parse a square bracket"
if not self.factory.detecttype(SquareBracket, pos):
return None
bracket = SquareBracket()
bracket.parsebit(pos)
self.add(bracket)
return bracket
def parseliteral(self, pos):
"Parse a literal bracket."
if not self.factory.detecttype(Bracket, pos):
Trace.error('No literal parameter found at: ' + pos.identifier())
return None
bracket = Bracket()
self.add(bracket.parseliteral(pos))
return bracket.literal
def parsesquareliteral(self, pos):
"Parse a square bracket literally."
if not self.factory.detecttype(SquareBracket, pos):
return None
bracket = SquareBracket()
self.add(bracket.parseliteral(pos))
return bracket.literal
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."
if self.original in self.commandmap:
self.output = FixedOutput()
self.html = [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)
if not self.factory.detecttype(Bracket, pos):
Trace.error('No parameter for ' + unicode(self))
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 = self.parseliteral(pos)
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
self.symbol = TaggedBit().constant(symbol, 'span class="symbolover"')
self.contents.append(self.symbol)
self.parameter = self.parseparameter(pos)
self.parameter.output = TaggedOutput().settag('span class="undersymbol"')
self.simplifyifpossible()
class UnderDecoratingFunction(DecoratingFunction):
"A function that decorates some bit of text from below."
commandmap = FormulaConfig.underdecoratingfunctions
def parsebit(self, pos):
"Parse an under-decorating function."
DecoratingFunction.parsebit(self, pos)
self.symbol.output.settag('span class="symbolunder"')
self.parameter.output.settag('span class="oversymbol"')
FormulaFactory.types += [FormulaCommand, SymbolFunction]
FormulaCommand.commandbits = [
EmptyCommand(), AlphaCommand(), OneParamFunction(), DecoratingFunction(),
FontFunction(), LabelFunction(), TextFunction(), UnderDecoratingFunction(),
]
class ParameterDefinition(object):
"The definition of a parameter in a hybrid function."
"[] parameters are optional, {} parameters are mandatory."
"Each parameter has a one-character name, like {$1} or {$p}."
"A parameter that ends in ! like {$p!} is a literal."
parambrackets = [('[', ']'), ('{', '}')]
def __init__(self):
self.name = None
self.literal = False
self.optional = False
self.value = None
self.literalvalue = None
def parse(self, pos):
"Parse a parameter definition: [$0], {$x}, {$1!}..."
for (opening, closing) in ParameterDefinition.parambrackets:
if pos.checkskip(opening):
if opening == '[':
self.optional = True
if not pos.checkskip('$'):
Trace.error('Wrong parameter name ' + pos.current())
return None
self.name = pos.currentskip()
if pos.checkskip('!'):
self.literal = True
if not pos.checkskip(closing):
Trace.error('Wrong parameter closing ' + pos.currentskip())
return None
return self
Trace.error('Wrong character in parameter template: ' + pos.currentskip())
return None
def read(self, pos, function):
"Read the parameter itself using the definition."
if self.literal:
if self.optional:
self.literalvalue = function.parsesquareliteral(pos)
else:
self.literalvalue = function.parseliteral(pos)
if self.literalvalue:
self.value = FormulaConstant(self.literalvalue)
elif self.optional:
self.value = function.parsesquare(pos)
else:
self.value = function.parseparameter(pos)
def __unicode__(self):
"Return a printable representation."
result = 'param ' + self.name
if self.value:
result += ': ' + unicode(self.value)
else:
result += ' (empty)'
return result
class HybridFunction(CommandBit):
"Read a function with a variable number of parameters, defined in a template."
commandmap = FormulaConfig.hybridfunctions
def parsebit(self, pos):
"Parse a function with [] and {} parameters"
readtemplate = self.translated[0]
writetemplate = self.translated[1]
self.readparams(readtemplate, pos)
self.contents = self.writeparams(writetemplate)
def readparams(self, readtemplate, pos):
"Read the params according to the template."
self.params = dict()
for paramdef in self.paramdefs(readtemplate):
paramdef.read(pos, self)
self.params['$' + paramdef.name] = paramdef
def paramdefs(self, readtemplate):
"Read each param definition in the template"
pos = TextPosition(readtemplate)
while not pos.finished():
paramdef = ParameterDefinition().parse(pos)
if paramdef:
yield paramdef
def writeparams(self, writetemplate):
"Write all params according to the template"
return self.writepos(TextPosition(writetemplate))
def writepos(self, pos):
"Write all params as read in the parse position."
result = []
while not pos.finished():
if pos.checkskip('$'):
param = self.writeparam(pos)
if param:
result.append(param)
elif pos.checkskip('f'):
function = self.writefunction(pos)
if function:
result.append(function)
else:
result.append(FormulaConstant(pos.currentskip()))
return result
def writeparam(self, pos):
"Write a single param of the form $0, $x..."
name = '$' + pos.currentskip()
if not name in self.params:
Trace.error('Unknown parameter ' + name)
return None
if not self.params[name]:
return None
if pos.checkskip('.'):
self.params[name].value.type = pos.globalpha()
return self.params[name].value
def writefunction(self, pos):
"Write a single function f0,...,fn."
tag = self.readtag(pos)
if not tag:
return None
if not pos.checkskip('{'):
Trace.error('Function should be defined in {}')
return None
pos.pushending('}')
contents = self.writepos(pos)
pos.popending()
if len(contents) == 0:
return None
function = TaggedBit().complete(contents, tag)
function.type = None
return function
def readtag(self, 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 variable in self.params:
if variable in tag:
param = self.params[variable]
if not param.literal:
Trace.error('Parameters in tag ' + tag + ' should be literal: {' + variable + '!}')
continue
if param.literalvalue:
value = param.literalvalue
else:
value = ''
tag = tag.replace(variable, 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 process(self):
"Read size parameters if present."
self.size = ContainerSize().readparameters(self)
def set(self, cell):
"Set alignments in the corresponding cell"
alignment = self.getparameter('alignment')
if alignment == 'block':
alignment = 'justify'
cell.setattribute('align', alignment)
valignment = self.getparameter('valignment')
cell.setattribute('valign', valignment)
self.size.addstyle(cell)
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) + '"'
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.checkforplain(row, index)
self.checkmulticolumn(row, index)
index += 1
return table
def longtable(self, table):
"Postprocess a long table, removing unwanted rows"
features = table.getparameter('features')
if not features:
return
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 row.getparameter(attrname):
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 row.getparameter(attrname):
row.output = EmptyOutput()
def checkforplain(self, row, index):
"Make plain layouts visible if necessary."
cell = row.contents[index]
plainlayouts = cell.searchall(PlainLayout)
if len(plainlayouts) <= 1:
return
for plain in plainlayouts:
plain.makevisible()
def checkmulticolumn(self, row, index):
"Process a multicolumn attribute"
cell = row.contents[index]
mc = cell.getparameter('multicolumn')
if not mc:
return
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
mc = row.contents[index].getparameter('multicolumn')
if mc != '2':
return False
return True
Postprocessor.stages.append(PostTable)
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"
vectorformats = ImageConfig.formats['vector']
rasterformats = ImageConfig.formats['raster']
defaultformat = ImageConfig.formats['default']
size = None
def __init__(self):
self.parser = InsetParser()
self.output = TaggedOutput()
self.type = 'embedded'
def process(self):
"Place the url, convert the image if necessary."
self.origin = InputPath(self.getparameter('filename'))
self.destination = self.getdestination(self.origin)
self.size = ContainerSize().readparameters(self)
if self.origin.exists():
ImageConverter.instance.convert(self)
else:
Trace.error('Image ' + unicode(self.origin) + ' not found')
self.setsize()
self.settag()
def getdestination(self, origin):
"Convert origin path to destination path."
"Changes extension of destination to output image format."
destination = OutputPath(origin)
if Options.noconvert:
return destination
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 setsize(self):
"Set the size attributes width and height."
width, height = ImageFile(self.destination).getdimensions()
self.size.checkimage(width, height)
def scalevalue(self, value):
"Scale the value according to the image scale and return it as unicode."
scaled = value * int(self.size.scale) / 100
return unicode(int(scaled)) + 'px'
def settag(self):
"Set the output tag for the image."
tag = 'img class="' + self.type + '"'
if self.origin.exists():
url = self.destination.url
else:
url = self.origin.url
alt = Translator.translate('figure') + ' ' + url
tag += ' src="' + url + '" alt="' + alt + '"'
emptytag = True
if self.destination.hasext('.svg'):
self.contents = [Constant(alt)]
tag = 'object class="' + self.type + '" data="' + url + '"'
emptytag = False
self.output.settag(tag, True, empty=emptytag)
self.size.addstyle(self)
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 or Options.noconvert:
return
if image.origin.path == image.destination.path:
return
if image.destination.exists():
if image.origin.getmtime() <= image.destination.getmtime():
# file has not changed; do not convert
return
image.destination.createdirs()
converter, command = self.buildcommand(image)
try:
Trace.debug(converter + ' command: "' + command + '"')
result = os.system(command.encode(sys.getfilesystemencoding()))
if result != 0:
Trace.error(converter + ' 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 buildcommand(self, image):
"Build the command to convert the image."
if not Options.converter in ImageConfig.converters:
Trace.error('Converter ' + Options.converter + ' not configured.')
ImageConverter.active = False
return ''
command = ImageConfig.converters[Options.converter]
params = self.getparams(image)
for param in params:
command = command.replace('$' + param, unicode(params[param]))
# remove unwanted options
while '[' in command and ']' in command:
command = self.removeparam(command)
return Options.converter, command
def removeparam(self, command):
"Remove an unwanted param."
if command.index('[') > command.index(']'):
Trace.error('Converter command should be [...$...]: ' + command)
exit()
before = command[:command.index('[')]
after = command[command.index(']') + 1:]
between = command[command.index('[') + 1:command.index(']')]
if '$' in between:
return before + after
return before + between + after
def getparams(self, image):
"Get the parameters for ImageMagick conversion"
params = dict()
params['input'] = image.origin
params['output'] = image.destination
if image.origin.hasexts(Image.vectorformats):
scale = 100
if image.size.scale:
scale = image.size.scale
# descale
image.size.scale = None
params['scale'] = 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()
elif self.path.hasext('.svg'):
dimensions = self.getsvgdimensions()
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 getsvgdimensions(self):
"Get the dimensions of a SVG image."
return (None, None)
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 ListItem(Container):
"An element in a list"
type = 'none'
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 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 Float(Container):
"A floating inset"
type = 'none'
def __init__(self):
self.parser = InsetParser()
self.output = TaggedOutput().settag('div class="float"', True)
self.parentfloat = None
self.children = []
def process(self):
"Get the float type."
self.type = self.header[2]
self.processfloats()
self.processtags()
self.chapter = NumberGenerator.instance.getchapter()
def processtags(self):
"Process the HTML tags."
tagged = self.embed()
self.applywideningtag(tagged)
def embed(self):
"Embed the whole contents in a div."
embeddedtag = self.getembeddedtag()
tagged = TaggedText().complete(self.contents, embeddedtag, True)
self.contents = [tagged]
return tagged
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 applywideningtag(self, container):
"Apply the tag to set float width, if present."
images = self.searchall(Image)
if len(images) != 1:
return ''
image = images[0]
if not image.size:
return
width = image.size.removepercentwidth()
if not width:
return
image.type = 'figure'
ContainerSize().setmax(width).addstyle(container)
image.settag()
def searchinside(self, type):
"Search for a given type in the contents"
return self.searchincontents(self.contents, type)
def searchincontents(self, contents, type):
"Search in the given contents for the required type."
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.searchincontents(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."
self.embed()
placement = self.getparameter('placement')
self.output.tag = 'div class="wrap-' + placement + '"'
self.applywideningtag(self)
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)
def create(self, message):
"Create a caption with a given message."
self.contents = [Constant(message)]
return self
class Listing(Container):
"A code listing"
processor = None
def __init__(self):
self.parser = InsetParser()
self.output = TaggedOutput().settag('div class="listing"', True)
self.numbered = None
def process(self):
"Remove all layouts"
self.counter = 0
self.type = 'listing'
self.processparams()
if Listing.processor:
Listing.processor.preprocess(self)
newcontents = []
for container in self.contents:
newcontents += self.extract(container)
tagged = TaggedText().complete(newcontents, 'pre class="listing"', False)
self.contents = [tagged]
if 'caption' in self.lstparams:
text = self.lstparams['caption'][1:-1]
self.contents.insert(0, Caption().create(text))
if Listing.processor:
Listing.processor.postprocess(self)
def processparams(self):
"Process listing parameteres."
LstParser().parsecontainer(self)
if 'numbers' in self.lstparams:
self.numbered = self.lstparams['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 FloatNumber(Container):
"Holds the number for a float in the caption."
def __init__(self):
self.output = ContentsOutput()
def create(self, float):
"Create the float number."
self.contents = [Constant(float.partkey.tocentry)]
return self
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"
self.postnumber(float)
number = FloatNumber().create(float)
for caption in float.searchinside(Caption):
self.postlabels(float, caption)
caption.contents.insert(0, Constant(u' '))
caption.contents.insert(0, number)
return float
def postlabels(self, float, caption):
"Search for labels and move them to the top"
labels = caption.searchremove(Label)
if len(labels) == 0:
labels = [Label().create(' ', float.partkey.tocentry.replace(' ', '-'))]
float.contents = labels + float.contents
def postnumber(self, float):
"Number a float if it isn't numbered."
if float.partkey:
return
if float.parentfloat:
self.postnumber(float.parentfloat)
index = float.parentfloat.children.index(float)
number = NumberGenerator.instance.letter(index).lower()
entry = '(' + number + ')'
else:
number = NumberGenerator.instance.generatechaptered(float.type, float.chapter)
entry = Translator.translate('float-' + float.type) + number
float.partkey = PartKey().createfloat(entry, number)
class PostWrap(PostFloat):
"For a wrap: exactly like a float"
processedclass = Wrap
Postprocessor.stages += [PostFloat, PostWrap]
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 NewPageInset(NewPage):
"A new page command."
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 SideNote(Container):
"A side note that appears at the right."
def __init__(self):
self.parser = InsetParser()
self.output = TaggedOutput()
def process(self):
"Enclose everything in a marginal span."
self.output.settag('span class="Marginal"', True)
class Footnote(Container):
"A footnote to the main text"
order = '-'
def __init__(self):
self.parser = InsetParser()
self.output = TaggedOutput().settag('span class="FootOuter"', False)
def process(self):
"Add a letter for the order, rotating"
if Options.numberfoot:
letter = NumberGenerator.instance.generateunique('Footnote')
else:
Footnote.order = NumberGenerator.instance.increase(Footnote.order)
letter = Footnote.order
span = 'span class="FootMarker"'
marker = TaggedText().constant('[' + letter + ']', span)
tag = TaggedText().complete([marker] + self.contents, 'span class="Foot"', True)
self.contents = [marker, tag]
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 FlexInset(Container):
"A flexible inset, generic version."
def __init__(self):
self.parser = InsetParser()
self.output = TaggedOutput().settag('span', False)
def process(self):
"Set the correct flex tag."
self.type = self.header[2]
if not self.type in TagConfig.flex:
Trace.error('Unknown Flex inset ' + self.type)
return
self.output.settag(TagConfig.flex[self.type], False)
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.getparameter('type')
self.contents = [Constant(self.getparameter('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]
self.output.settag('div class="' + self.type + '"', True)
ContainerSize().readparameters(self).addstyle(self)
class IncludeInset(Container):
"A child document included within another."
# the converter factory will be set in converter.py
converterfactory = None
filename = None
def __init__(self):
self.parser = InsetParser()
self.output = ContentsOutput()
def process(self):
"Include the provided child document"
self.filename = os.path.join(Options.directory, self.getparameter('filename'))
Trace.debug('Child document: ' + self.filename)
LstParser().parsecontainer(self)
command = self.getparameter('LatexCommand')
if command == 'verbatiminput':
self.readverbatim()
return
elif command == 'lstinputlisting':
self.readlisting()
return
self.processinclude()
def processinclude(self):
"Process a regular include: standard child document."
self.contents = []
olddir = Options.directory
newdir = os.path.dirname(self.getparameter('filename'))
if newdir != '':
Trace.debug('Child dir: ' + newdir)
Options.directory = os.path.join(Options.directory, newdir)
try:
self.convertinclude()
finally:
Options.directory = olddir
def convertinclude(self):
"Convert an included document."
try:
converter = IncludeInset.converterfactory.create(self)
except:
Trace.error('Could not read ' + self.filename + ', please check that the file exists and has read permissions.')
return
if self.hasemptyoutput():
return
converter.convert()
self.contents = converter.getcontents()
def hasemptyoutput(self):
"Check if the parent's output is empty."
current = self.parent
while current:
if isinstance(current.output, EmptyOutput):
return True
current = current.parent
return False
def readverbatim(self):
"Read a verbatim document."
self.contents = [TaggedText().complete(self.readcontents(), 'pre', True)]
def readlisting(self):
"Read a document as a listing."
listing = Listing()
listing.contents = self.readcontents()
listing.parameters = self.parameters
listing.process()
self.contents = [listing]
def readcontents(self):
"Read the contents of a complete file."
contents = list()
lines = BulkFile(self.filename).readall()
for line in lines:
contents.append(Constant(line))
return contents
def __unicode__(self):
"Return a printable description."
if not self.filename:
return 'Included unnamed file'
return 'Included "' + self.filename + '"'
class ChangeInserted(Container):
"A change which consists of an insertion."
def __init__(self):
self.parser = TextParser(self)
self.output = ContentsOutput()
class ChangeDeleted(TaggedText):
"A change which consists of a deletion."
def __init__(self):
self.parser = TextParser(self)
self.output = EmptyOutput()
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):
self.factory.clearignored(pos)
if pos.finished():
return
if not self.factory.detecttype(WholeFormula, pos):
Trace.error('Unexpected end of array cell at ' + pos.identifier())
pos.skip(pos.current())
return
formula = WholeFormula()
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.factory = self.factory
cell.parsebit(pos)
self.add(cell)
index += 1
pos.checkskip(FormulaRow.cellseparator)
if len(self.contents) == 0:
self.output = EmptyOutput()
class MultiRowFormula(CommandBit):
"A formula with multiple rows."
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)
row = FormulaRow(self.alignments)
row.factory = self.factory
yield row
if pos.checkfor(rowseparator):
self.original += pos.popending(rowseparator)
else:
return
class FormulaArray(MultiRowFormula):
"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'
literal = self.parsesquareliteral(pos)
if literal:
self.valign = literal
# horizontal
literal = self.parseliteral(pos)
self.alignments = []
for l in literal:
self.alignments.append(l)
class FormulaCases(MultiRowFormula):
"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 EquationEnvironment(MultiRowFormula):
"A \\begin{}...\\end equation environment with rows and cells."
def parsebit(self, pos):
"Parse the whole environment."
self.output = TaggedOutput().settag('table class="environment"', True)
environment = self.piece.replace('*', '')
if environment in FormulaConfig.environments:
self.alignments = FormulaConfig.environments[environment]
else:
Trace.error('Unknown equation environment ' + self.piece)
self.alignments = ['l']
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()]
def parsebit(self, pos):
"Parse the begin command"
literal = self.parseliteral(pos)
bit = self.findbit(literal)
ending = FormulaConfig.array['end'] + '{' + 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:
newbit = Cloner.clone(bit)
newbit.factory = self.factory
return newbit
bit = EquationEnvironment()
bit.factory = self.factory
bit.piece = piece
return bit
FormulaCommand.commandbits += [BeginCommand()]
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 = Translator.translate('bibliography')
tag = TaggedText().constant(bibliography, 'h1 class="biblio"', True)
self.contents.append(tag)
files = self.getparameter('bibfiles').split(',')
showall = False
if self.getparameter('btprint') == 'btPrintAll':
showall = True
for file in files:
bibfile = BibFile(file, showall)
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']
entry.citetemplate = style['cite']
type = entry.type.lower()
if type in style:
entry.template = style[type]
entry.process()
self.contents.append(entry)
def readstyle(self):
"Read the style from the bibliography options"
options = self.getparameter('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, showall):
"Create the BibTeX file"
self.filename = filename + '.bib'
self.showall = showall
self.added = 0
self.ignored = 0
self.entries = []
def parse(self):
"Parse the BibTeX file and extract all entries."
try:
self.parsefile()
except IOError:
Trace.error('Error reading ' + self.filename + '; make sure the file exists and can be read.')
def parsefile(self):
"Parse the whole file."
bibpath = InputPath(self.filename)
if Options.lowmem:
pos = FilePosition(bibpath.path)
else:
bulkfile = BulkFile(bibpath.path)
text = ''.join(bulkfile.readall())
pos = TextPosition(text)
while not pos.finished():
pos.skipspace()
if pos.checkskip(','):
pos.skipspace()
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 self.showall or newentry.isreferenced():
self.entries.append(newentry)
self.added += 1
else:
self.ignored += 1
return
# Skip a single character and show it as an error
skipped = pos.globincluding('\n').strip()
Trace.error('Unidentified BibTeX entry, skipped "' + skipped + '"')
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 = []
def lineerror(self, message, pos):
"Show an error message for a line."
Trace.error(message + ': ' + pos.identifier())
class CommentEntry(Entry):
"A simple comment."
def detect(self, pos):
"Detect the special entry"
return pos.checkfor('%')
def parse(self, pos):
"Parse all consecutive comment lines."
while pos.checkfor('%'):
pos.globincluding('\n')
def isreferenced(self):
"A comment entry is never referenced"
return False
def __unicode__(self):
"Return a string representation"
return 'Comment'
class ContentEntry(Entry):
"An entry holding some content."
nameseparators = ['{', '=', '"', '#']
valueseparators = ['{', '"', '#', '\\', '}']
escaped = BibTeXConfig.escaped
replaced = BibTeXConfig.replaced
replacedinitials = [x[0] for x in BibTeXConfig.replaced]
def __init__(self):
self.key = None
self.tags = dict(BibStylesConfig.defaulttags)
self.output = TaggedOutput().settag('p class="biblio"', True)
def parse(self, pos):
"Parse the entry between {}"
self.type = self.parseexcluding(pos, self.nameseparators).strip()
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
pos.pushending(',', True)
self.parsetag(pos)
if pos.checkfor(','):
pos.popending(',')
def parsetag(self, pos):
"Parse a single tag."
pos.skipspace()
piece = self.parseexcluding(pos, self.nameseparators)
if pos.finished():
self.key = piece
return
if not pos.checkskip('='):
self.lineerror('Undesired character in tag name ' + piece, pos)
return
name = piece.lower().strip()
pos.skipspace()
value = self.parsevalue(pos)
self.tags[name] = value
if hasattr(self, 'dissect' + name):
dissector = getattr(self, 'dissect' + name)
dissector(value)
if not pos.finished():
remainder = pos.globexcluding(',')
self.lineerror('Ignored ' + remainder + ' before comma', pos)
def parsevalue(self, pos):
"Parse the value for a tag"
pos.skipspace()
if pos.checkfor(','):
self.lineerror('Unexpected ,', pos)
return ''
return self.parserecursive(pos, True)
def parserecursive(self, pos, initial=False):
"Parse brackets or quotes recursively."
contents = ''
while not pos.finished():
contents += self.parseexcluding(pos, self.valueseparators)
if pos.finished():
return contents
elif pos.checkfor('{'):
contents += self.parsebracket(pos)
elif pos.checkfor('"'):
contents += self.parsequoted(pos, initial)
elif pos.checkfor('\\'):
contents += self.parseescaped(pos)
elif pos.checkfor('#'):
contents += self.parsehash(pos, initial)
else:
self.lineerror('Unexpected character ' + pos.current(), pos)
pos.currentskip()
return contents
def parseescaped(self, pos):
"Parse an escaped string \\*."
if not pos.checkskip('\\'):
self.lineerror('Not an escaped character', pos)
return ''
escaped = '\\'
if pos.checkskip('{'):
escaped += pos.currentskip()
if not pos.checkskip('}'):
self.lineerror('Weird escaped but unclosed brackets \\{*', pos)
if not escaped in ContentEntry.escaped:
self.lineerror('Unknown escaped character ' + escaped, pos)
return escaped[1:]
return ContentEntry.escaped[escaped]
for key in ContentEntry.escaped:
if pos.checkskip(key[1:]):
return ContentEntry.escaped[key]
if pos.current().isalpha():
alpha = '\\' + pos.globalpha()
if alpha in FormulaConfig.commands:
return FormulaConfig.commands[alpha]
self.lineerror('Unknown escaped command \\' + alpha, pos)
return ''
self.lineerror('Unknown escaped string \\' + pos.current(), pos)
return pos.currentskip()
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, initial):
"Parse a piece of quoted text"
if not pos.checkskip('"'):
self.lineerror('Missing opening " in quote', pos)
return ''
if not initial:
return '"'
pos.pushending('"', True)
quoted = self.parserecursive(pos)
pos.popending('"')
pos.skipspace()
return quoted
def parsehash(self, pos, initial):
"Parse a hash mark #."
if not pos.checkskip('#'):
self.lineerror('Missing # in hash', pos)
return ''
if not initial:
return '#'
pos.skipspace()
return ''
def parseexcluding(self, pos, undesired):
"Parse a piece not structure (including spaces)."
result = ''
while not pos.finished():
if pos.current() in undesired:
return result
if pos.current().isspace():
result += ' '
pos.skipspace()
else:
replaced = self.parsereplaced(pos)
if replaced:
result += replaced
else:
result += pos.currentskip()
return result
def parsereplaced(self, pos):
"Check for one of the replaced strings."
if not pos.current() in ContentEntry.replacedinitials:
return None
for key in ContentEntry.replaced:
if pos.checkskip(key):
return ContentEntry.replaced[key]
return None
def dissectauthor(self, authortag):
"Dissect the author tag into pieces."
authorsplit = authortag.split(' and ')
if len(authorsplit) == 0:
return
authorlist = []
for authorname in authorsplit:
author = BibAuthor().parse(authorname)
authorlist.append(author)
initials = ''
authors = ''
if len(authorlist) == 1:
initials = authorlist[0].surname[0:3]
authors = unicode(authorlist[0])
else:
for author in authorlist:
initials += author.surname[0:1]
authors += unicode(author) + ', '
authors = authors[:-2]
self.tags['surname'] = authorlist[0].surname
self.tags['Sur'] = initials
self.tags['authors'] = authors
def dissectyear(self, yeartag):
"Dissect the year tag into pieces, looking for 4 digits in a row."
pos = TextPosition(yeartag)
while not pos.finished():
if pos.current().isdigit():
number = pos.globnumber()
if len(number) == 4:
self.tags['YY'] = number[2:]
return
else:
pos.currentskip()
def dissectfile(self, filetag):
"Extract the filename from the file tag as ':filename:FORMAT'."
if not filetag.startswith(':'):
return
bits = filetag.split(':')
if len(bits) != 3:
return
self.tags['filename'] = bits[1]
self.tags['format'] = bits[2]
class BibAuthor(object):
"A BibTeX individual author."
def __init__(self):
self.surname = ''
self.firstnames = []
def parse(self, tag):
"Parse an individual author tag."
if ',' in tag:
self.parsecomma(tag)
else:
self.parsewithoutcomma(tag)
return self
def parsecomma(self, tag):
"Parse an author with a comma: Python, M."
bits = tag.split(',')
if len(bits) > 2:
Trace.error('Too many commas in ' + tag)
self.surname = bits[0].strip()
self.parsefirstnames(bits[1].strip())
def parsewithoutcomma(self, tag):
"Parse an author without a comma: M. Python."
bits = tag.rsplit(None, 1)
self.surname = bits[-1].strip()
if len(bits) == 1:
return
self.parsefirstnames(bits[0].strip())
def parsefirstnames(self, firstnames):
"Parse the first name."
for firstname in firstnames.split():
self.firstnames.append(firstname)
def getinitial(self):
"Get the main initial for the author."
if len(self.surname) == 0:
return ''
return self.surname[0].toupper()
def __unicode__(self):
"Return a printable representation."
result = ''
for firstname in self.firstnames:
result += firstname + ' '
return result + self.surname
class SpecialEntry(ContentEntry):
"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
Entry.entries += [CommentEntry(), SpecialEntry()]
class PubEntry(ContentEntry):
"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 BiblioReference.references
def process(self):
"Process the entry"
self.index = NumberGenerator.instance.generateunique('pubentry')
self.tags['index'] = self.index
biblio = BiblioEntry()
biblio.citeref = self.createref()
biblio.processcites(self.key)
self.contents = [biblio, Constant(' ')]
self.contents += self.entrycontents()
def entrycontents(self):
"Get the contents of the entry."
return self.translatetemplate(self.template)
def createref(self):
"Create the reference to cite."
return self.translatetemplate(self.citetemplate)
def translatetemplate(self, template):
"Translate a complete template into a list of contents."
pos = TextPosition(template)
part = BibPart(self.tags).parse(pos)
for variable in part.searchall(BibVariable):
if variable.empty():
Trace.error('Error parsing BibTeX template for ' + unicode(self) + ': '
+ unicode(variable) + ' is empty')
return [part]
def __unicode__(self):
"Return a string representation"
string = ''
if 'author' in self.tags:
string += self.tags['author'] + ': '
if 'title' in self.tags:
string += '"' + self.tags['title'] + '"'
return string
class BibPart(Container):
"A part of a BibTeX template."
def __init__(self, tags):
self.output = ContentsOutput()
self.contents = []
self.tags = tags
self.quotes = 0
def parse(self, pos):
"Parse a part of a template, return a list of contents."
while not pos.finished():
self.add(self.parsepiece(pos))
return self
def parsepiece(self, pos):
"Get the next piece of the template, return if it was empty."
if pos.checkfor('{'):
return self.parsebraces(pos)
elif pos.checkfor('$'):
return self.parsevariable(pos)
result = ''
while not pos.finished() and not pos.current() in '{$':
if pos.current() == '"':
self.quotes += 1
result += pos.currentskip()
return Constant(result)
def parsebraces(self, pos):
"Parse a pair of curly braces {}."
if not pos.checkskip('{'):
Trace.error('Missing { in braces.')
return None
pos.pushending('}')
part = BibPart(self.tags).parse(pos)
pos.popending('}')
empty = part.emptyvariables()
if empty:
return None
return part
def parsevariable(self, pos):
"Parse a variable $name."
var = BibVariable(self.tags).parse(pos)
if self.quotes % 2 == 1:
# odd number of quotes; don't add spans in an attribute
var.removetag()
return var
def emptyvariables(self):
"Find out if there are only empty variables in the part."
for variable in self.searchall(BibVariable):
if not variable.empty():
return False
return True
def add(self, piece):
"Add a new piece to the current part."
if not piece:
return
if self.redundantdot(piece):
# remove extra dot
piece.string = piece.string[1:]
self.contents.append(piece)
piece.parent = self
def redundantdot(self, piece):
"Find out if there is a redundant dot in the next piece."
if not isinstance(piece, Constant):
return False
if not piece.string.startswith('.'):
return False
if len(self.contents) == 0:
return False
if not isinstance(self.contents[-1], BibVariable):
return False
if not self.contents[-1].extracttext().endswith('.'):
return False
return True
class BibVariable(Container):
"A variable in a BibTeX template."
def __init__(self, tags):
self.output = TaggedOutput()
self.contents = []
self.tags = tags
def parse(self, pos):
"Parse the variable name."
if not pos.checkskip('$'):
Trace.error('Missing $ in variable name.')
return self
self.key = pos.globalpha()
self.output.tag = 'span class="bib-' + self.key + '"'
self.processtags()
return self
def processtags(self):
"Find the tag with the appropriate key in the list of tags."
if not self.key in self.tags:
return
result = self.tags[self.key].strip()
self.contents = [Constant(result)]
def empty(self):
"Find out if the variable is empty."
if not self.contents:
return True
if self.extracttext() == '':
return True
return False
def removetag(self):
"Remove the output tag and leave just the contents."
self.output = ContentsOutput()
def __unicode__(self):
"Return a printable representation."
result = 'variable ' + self.key
if not self.empty():
result += ':' + self.extracttext()
return result
Entry.entries += [PubEntry()]
class NewfangledChunk(Layout):
"A chunk of literate programming."
names = dict()
firsttime = True
def process(self):
"Process the literate chunk."
self.output.tag = 'div class="chunk"'
self.type = 'chunk'
text = self.extracttext()
parts = text.split(',')
if len(parts) < 1:
Trace.error('Not enough parameters in ' + text)
return
self.name = parts[0]
self.number = self.order()
self.createlinks()
self.contents = [self.left, self.declaration(), self.right]
ChunkProcessor.lastchunk = self
def order(self):
"Create the order number for the chunk."
return NumberGenerator.instance.generateunique('chunk')
def createlinks(self):
"Create back and forward links."
self.leftlink = Link().complete(self.number, 'chunk:' + self.number, type='chunk')
self.left = TaggedText().complete([self.leftlink], 'span class="chunkleft"', False)
self.right = TaggedText().constant('', 'span class="chunkright"', False)
if not self.name in NewfangledChunk.names:
NewfangledChunk.names[self.name] = []
else:
last = NewfangledChunk.names[self.name][-1]
forwardlink = Link().complete(self.number + u'→', 'chunkback:' + last.number, type='chunk')
backlink = Link().complete(u'←' + last.number + u' ', 'chunkforward:' + self.number, type='chunk')
forwardlink.setmutualdestination(backlink)
last.right.contents.append(forwardlink)
self.right.contents.append(backlink)
NewfangledChunk.names[self.name].append(self)
self.origin = self.createorigin()
if self.name in NewfangledChunkRef.references:
for ref in NewfangledChunkRef.references[self.name]:
self.linkorigin(ref.origin)
def createorigin(self):
"Create a link that points to the chunks' origin."
link = Link()
self.linkorigin(link)
return link
def linkorigin(self, link):
"Create a link to the origin."
start = NewfangledChunk.names[self.name][0]
link.complete(start.number, type='chunk')
link.destination = start.leftlink
link.computedestination()
def declaration(self):
"Get the chunk declaration."
contents = []
text = u'⟨' + self.name + '[' + unicode(len(NewfangledChunk.names[self.name])) + '] '
contents.append(Constant(text))
contents.append(self.origin)
text = ''
if NewfangledChunk.firsttime:
Listing.processor = ChunkProcessor()
NewfangledChunk.firsttime = False
text += u'⟩'
if len(NewfangledChunk.names[self.name]) > 1:
text += '+'
text += u'≡'
contents.append(Constant(text))
return TaggedText().complete(contents, 'span class="chunkdecl"', True)
class ChunkProcessor(object):
"A processor for listings that belong to chunks."
lastchunk = None
counters = dict()
endcommand = '}'
chunkref = 'chunkref'
def preprocess(self, listing):
"Preprocess a listing: set the starting counter."
if not ChunkProcessor.lastchunk:
return
name = ChunkProcessor.lastchunk.name
if not name in ChunkProcessor.counters:
ChunkProcessor.counters[name] = 0
listing.counter = ChunkProcessor.counters[name]
for command, container, index in self.commandsinlisting(listing):
chunkref = self.getchunkref(command)
if chunkref:
self.insertchunkref(chunkref, container, index)
def commandsinlisting(self, listing):
"Find all newfangle commands in a listing."
for container in listing.contents:
for index in range(len(container.contents) - 2):
if self.findinelement(container, index):
third = container.contents[index + 2].string
end = third.index(NewfangleConfig.constants['endmark'])
command = third[:end]
lenstart = len(NewfangleConfig.constants['startmark'])
container.contents[index].string = container.contents[index].string[:-lenstart]
del container.contents[index + 1]
container.contents[index + 1].string = third[end + len(NewfangleConfig.constants['endmark']):]
yield command, container, index
def findinelement(self, container, index):
"Find a newfangle command in an element."
for i in range(2):
if not isinstance(container.contents[index + i], StringContainer):
return False
first = container.contents[index].string
second = container.contents[index + 1].string
third = container.contents[index + 2].string
if not first.endswith(NewfangleConfig.constants['startmark']):
return False
if second != NewfangleConfig.constants['startcommand']:
return False
if not NewfangleConfig.constants['endmark'] in third:
return False
return True
def getchunkref(self, command):
"Get the contents of a chunkref command, if present."
if not command.startswith(NewfangleConfig.constants['chunkref']):
return None
if not NewfangleConfig.constants['endcommand'] in command:
return None
start = len(NewfangleConfig.constants['chunkref'])
end = command.index(NewfangleConfig.constants['endcommand'])
return command[start:end]
def insertchunkref(self, ref, container, index):
"Insert a chunkref after the given index at the given container."
chunkref = NewfangledChunkRef().complete(ref)
container.contents.insert(index + 1, chunkref)
def postprocess(self, listing):
"Postprocess a listing: store the ending counter for next chunk."
if not ChunkProcessor.lastchunk:
return
ChunkProcessor.counters[ChunkProcessor.lastchunk.name] = listing.counter
class NewfangledChunkRef(Inset):
"A reference to a chunk."
references = dict()
def process(self):
"Show the reference."
self.output.tag = 'span class="chunkref"'
self.ref = self.extracttext()
self.addbits()
def complete(self, ref):
"Complete the reference to the given string."
self.output = ContentsOutput()
self.ref = ref
self.contents = [Constant(self.ref)]
self.addbits()
return self
def addbits(self):
"Add the bits to the reference."
if not self.ref in NewfangledChunkRef.references:
NewfangledChunkRef.references[self.ref] = []
NewfangledChunkRef.references[self.ref].append(self)
if self.ref in NewfangledChunk.names:
start = NewfangledChunk.names[self.ref][0]
self.origin = start.createorigin()
else:
self.origin = Link()
self.contents.insert(0, Constant(u'⟨'))
self.contents.append(Constant(' '))
self.contents.append(self.origin)
self.contents.append(Constant(u'⟩'))
def __unicode__(self):
"Return a printable representation."
return 'Reference to chunk ' + self.ref
class LyXPreamble(Container):
"The preamble at the beginning of a LyX file. Parsed for macros."
def __init__(self):
self.parser = PreambleParser()
self.output = EmptyOutput()
def process(self):
"Parse the LyX preamble, if needed."
if len(PreambleParser.preamble) == 0:
return
FormulaCommand.preambling = True
pos = TextPosition('\n'.join(PreambleParser.preamble))
while not pos.finished():
if self.detectdefinition(pos):
self.parsedefinition(pos)
else:
pos.globincluding('\n')
PreambleParser.preamble = []
FormulaCommand.preambling = False
def detectdefinition(self, pos):
"Detect a macro definition."
for function in FormulaConfig.definingfunctions:
if pos.checkfor(function):
return True
return False
def parsedefinition(self, pos):
"Parse a macro definition."
command = FormulaCommand()
command.factory = FormulaFactory()
bit = command.parsebit(pos)
if not isinstance(bit, DefiningFunction):
Trace.error('Did not define a macro with ' + unicode(bit))
class MathMacro(object):
"A math macro: command, parameters, default values, definition."
macros = dict()
def __init__(self):
self.newcommand = None
self.parameternumber = 0
self.defaults = []
self.definition = None
def instantiate(self):
"Return an instance of the macro."
return self.definition.clone()
class MacroParameter(FormulaBit):
"A parameter from a macro."
def detect(self, pos):
"Find a macro parameter: #n."
return pos.checkfor('#')
def parsebit(self, pos):
"Parse the parameter: #n."
if not pos.checkskip('#'):
Trace.error('Missing parameter start #.')
return
self.number = int(pos.currentskip())
self.original = '#' + unicode(self.number)
self.contents = [TaggedBit().constant('#' + unicode(self.number), 'span class="unknown"')]
class DefiningFunction(HybridFunction):
"Read a function that defines a new command (a macro)."
commandmap = FormulaConfig.definingfunctions
def parsebit(self, pos):
"Parse a function with [] and {} parameters."
if self.factory.detecttype(Bracket, pos):
newcommand = self.parseliteral(pos)
elif self.factory.detecttype(FormulaCommand, pos):
newcommand = FormulaCommand().extractcommand(pos)
else:
Trace.error('Unknown formula bit in defining function at ' + pos.identifier())
return
Trace.debug('New command: ' + newcommand)
HybridFunction.parsebit(self, pos)
macro = MathMacro()
macro.newcommand = newcommand
macro.parameternumber = self.readparameternumber()
macro.definition = self.params['$d'].value
self.extractdefaults(macro)
MathMacro.macros[newcommand] = macro
def readparameternumber(self):
"Read the number of parameters in the macro."
if not self.params['$n'].literalvalue:
return 0
return int(self.params['$n'].literalvalue)
def extractdefaults(self, macro):
"Extract the default values for existing parameters."
for index in range(9):
value = self.extractdefault(index + 1)
if value:
macro.defaults.append(value)
else:
return
def extractdefault(self, index):
"Extract the default value for parameter index."
value = self.params['$' + unicode(index)].value
if not value:
return None
if len(value.contents) == 0:
return FormulaConstant('')
return value.contents[0]
class MacroFunction(CommandBit):
"A function that was defined using a macro."
commandmap = MathMacro.macros
def parsebit(self, pos):
"Parse a number of input parameters."
self.values = []
macro = self.translated
while self.factory.detecttype(Bracket, pos):
self.values.append(self.parseparameter(pos))
defaults = list(macro.defaults)
remaining = macro.parameternumber - len(self.values) - len(defaults)
if remaining > 0:
self.parsenumbers(remaining, pos)
while len(self.values) < macro.parameternumber and len(defaults) > 0:
self.values.insert(0, defaults.pop())
if len(self.values) < macro.parameternumber:
Trace.error('Missing parameters in macro ' + unicode(self))
self.completemacro(macro)
def parsenumbers(self, remaining, pos):
"Parse the remaining parameters as a running number."
"For example, 12 would be {1}{2}."
if pos.finished():
return
if not self.factory.detecttype(Number, pos):
return
number = Number()
number.parsebit(pos)
if not len(number.original) == remaining:
self.values.append(number)
return
for digit in number.original:
value = Number()
value.add(FormulaConstant(digit))
value.type = number
self.values.append(value)
def completemacro(self, macro):
"Complete the macro with the parameters read."
self.contents = [macro.instantiate()]
for parameter in self.searchall(MacroParameter):
index = parameter.number - 1
if index >= len(self.values):
return
parameter.contents = [self.values[index].clone()]
class FormulaMacro(Formula):
"A math macro defined in an inset."
def __init__(self):
self.parser = MacroParser()
self.output = EmptyOutput()
def __unicode__(self):
"Return a printable representation."
return 'Math macro'
FormulaCommand.commandbits += [
DefiningFunction(), MacroFunction(),
]
FormulaFactory.types += [ MacroParameter ]
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
container = Cloner.create(self.tree.find(reader))
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
self.parsecontents(container, reader)
container.parameters = parser.parameters
container.parser = None
def parsecontents(self, container, reader):
"Parse the contents of a container."
contents = container.parser.parse(reader)
if isinstance(contents, basestring):
# read a string, set as parsed
container.parsed = contents
container.contents = []
else:
container.contents = contents
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."
def __init__(self):
Container.__init__(self)
self.branches = []
def create(self, container):
"Create the TOC entry for a container, consisting of a single link."
if container.partkey.header:
return self.header(container)
labels = container.searchall(Label)
if len(labels) == 0 or Options.toc:
url = Options.toctarget + '#' + container.partkey.partkey
link = Link().complete(container.partkey.tocentry, url=url)
else:
label = labels[0]
link = Link().complete(container.partkey.tocentry)
link.destination = label
self.contents = [link]
titlecontents = self.gettitlecontents(container)
if container.partkey.tocsuffix and titlecontents:
link.contents.append(Constant(container.partkey.tocsuffix))
link.contents += titlecontents
self.output = TaggedOutput().settag('div class="toc"', True)
self.partkey = container.partkey
return self
def header(self, container):
"Create a TOC entry for header and footer (0 depth)."
self.partkey = container.partkey
self.output = EmptyOutput()
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
extractor = ContainerExtractor(TOCConfig.extracttitle)
captions = container.searchall(Caption)
if len(captions) == 1:
return extractor.extract(captions[0])
return extractor.extract(container)
def __unicode__(self):
"Return a printable representation."
if not self.partkey.tocentry:
return 'Unnamed TOC entry'
return 'TOC entry: ' + self.partkey.tocentry
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 IndentedEntry(Container):
"An entry with an indentation."
def __init__(self):
self.output = ContentsOutput()
def create(self, indent, entry):
"Create the indented entry."
self.contents = [indent, entry]
return self
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.partkey.level:
self.tree.append(None)
if len(self.tree) > entry.partkey.level:
self.tree = self.tree[:entry.partkey.level]
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 convertindented(self, container):
"Convert a container into an indented TOC entry."
entry = self.convert(container)
if not entry:
return BlackBox()
return self.indent(entry)
def indent(self, entry):
"Indent a TOC entry."
indent = self.indenter.getindent(entry.partkey.level)
return IndentedEntry().create(indent, entry)
def convert(self, container):
"Convert a container to a TOC entry."
if not container.partkey:
return None
if container.partkey.partkey in self.cache:
return TOCConverter.cache[container.partkey.partkey]
if container.partkey.level > DocumentParameters.tocdepth:
return None
entry = TOCEntry().create(container)
TOCConverter.cache[container.partkey.partkey] = entry
TOCConverter.tree.store(entry)
return entry
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().process().gethtml())
return self
def write(self, container):
"Write the table of contents for a container."
entry = self.converter.convertindented(container)
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 IntegralTOC(IntegralProcessor):
"A processor for an integral TOC."
processedtype = TableOfContents
tocentries = []
def processeach(self, toc):
"Fill in a Table of Contents."
converter = TOCConverter()
for container in PartKeyGenerator.partkeyed:
toc.add(converter.convertindented(container))
# finish off with the footer to align indents
toc.add(converter.convertindented(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('cite', 'biblio-' + number, type='biblioentry')
link.contents = entry.citeref
entry.contents = [Constant('['), link, Constant('] ')]
if entry.key in BiblioCite.cites:
for cite in BiblioCite.cites[entry.key]:
cite.contents = entry.citeref
cite.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
return TOCEntry().create(float)
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 = reference.destination.labelnumber
if text:
reference.labelnumber = text
reference.format()
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 = [
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 SplitPartLink(IntegralProcessor):
"A link processor for multi-page output."
processedtype = Link
def processeach(self, link):
"Process each link and add the current page."
link.page = self.page
class UpAnchor(Link):
"An anchor to the top of the page for the up links."
def create(self, container):
"Create the up anchor based on the first container."
if not container.partkey:
Trace.error('No part key for ' + unicode(container))
return None
return self.createliteral(container.partkey.tocentry)
def createmain(self):
"Create the up anchor for the main page."
return self.createliteral(Translator.translate('main-page'))
def createliteral(self, literal):
"Create the up anchor based on a literal string."
self.complete('', '')
self.output = EmptyOutput()
self.partkey = PartKey().createanchor(literal)
return self
class SplitPartNavigation(object):
"Used to create the navigation links for a new split page."
def __init__(self):
self.upanchors = []
self.lastcontainer = None
self.nextlink = None
self.lastnavigation = None
def writefirstheader(self, basket):
"Write the first header to the basket."
anchor = self.createmainanchor()
basket.write(anchor)
basket.write(self.createnavigation(anchor))
def writeheader(self, basket, container):
"Write the header to the basket."
basket.write(LyXHeader().process())
basket.write(self.currentupanchor(container))
basket.write(self.createnavigation(container))
def writefooter(self, basket):
"Write the footer to the basket."
if self.lastnavigation:
basket.write(self.lastnavigation)
basket.write(LyXFooter())
def createnavigation(self, container):
"Create the navigation bar with all links."
prevlink = Link().complete(' ', 'prev', type='prev')
if self.nextlink:
self.setlinkname(prevlink, Translator.translate('prev'), self.lastcontainer)
self.setlinkname(self.nextlink, Translator.translate('next'), container)
prevlink.setmutualdestination(self.nextlink)
self.nextlink = Link().complete(' ', Translator.translate('next'), type='next')
uplink = Link().complete(Translator.translate('up'), url='', type='up')
self.setlinkname(uplink, Translator.translate('up'), self.getupdestination(container))
uplink.destination = self.getupdestination(container)
prevcontainer = TaggedText().complete([prevlink], 'span class="prev"')
nextcontainer = TaggedText().complete([self.nextlink], 'span class="next"')
upcontainer = TaggedText().complete([uplink], 'span class="up"')
contents = [prevcontainer, Constant('\n'), upcontainer, Constant('\n'), nextcontainer]
header = TaggedText().complete(contents, 'div class="splitheader"', True)
self.lastcontainer = container
self.lastnavigation = header
return header
def currentupanchor(self, container):
"Update the internal list of up anchors, and return the current one."
level = self.getlevel(container)
while len(self.upanchors) > level:
del self.upanchors[-1]
while len(self.upanchors) < level:
self.upanchors.append(self.upanchors[-1])
upanchor = UpAnchor().create(container)
self.upanchors.append(upanchor)
return upanchor
def createmainanchor(self):
"Create the up anchor to the main page."
mainanchor = UpAnchor().createmain()
self.upanchors.append(mainanchor)
return mainanchor
def getupdestination(self, container):
"Get the name of the up page."
level = self.getlevel(container)
if len(self.upanchors) < level:
uppage = self.upanchors[-1]
else:
uppage = self.upanchors[level - 1]
return uppage
def getlevel(self, container):
"Get the level of the container."
if not container.partkey:
return 1
else:
return container.partkey.level + 1
def setlinkname(self, link, type, container):
"Set the name on the link."
if not container.partkey:
Trace.error('No part key for link name ' + unicode(container))
return
link.contents = [Constant(type + ': ' + container.partkey.tocentry)]
class SplitTOCBasket(MemoryBasket):
"A memory basket which contains a split table of contents."
def __init__(self):
MemoryBasket.__init__(self)
self.entrycount = 0
self.root = None
self.converter = TOCConverter()
def write(self, container):
"Keep track of numbered layouts."
MemoryBasket.write(self, container)
if not container.partkey:
return
if container.partkey.header:
return
entry = self.converter.convert(container)
if not entry:
return
self.entrycount += 1
self.root = entry
def addtoc(self):
"Add the table of contents if necessary."
if self.entrycount != 1:
return
if self.root.branches == []:
return
text = Translator.translate('toc-for') + self.root.partkey.tocentry
toc = TableOfContents().create(text)
self.addbranches(self.root, toc)
toc.add(self.converter.convertindented(LyXFooter()))
self.write(toc)
def addbranches(self, entry, toc):
"Add an entry and all of its branches to the table of contents."
for branch in entry.branches:
toc.add(self.converter.indent(branch))
self.addbranches(branch, toc)
class SplitPartBasket(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.filename = writer.filename
self.converter = TOCConverter()
self.basket = MemoryBasket()
self.basket.page = writer.filename
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.firstbasket()
navigation = SplitPartNavigation()
for container in self.basket.contents:
if self.mustsplit(container):
filename = self.getfilename(container)
Trace.debug('New page ' + filename)
basket.addtoc()
navigation.writefooter(basket)
basket = self.addbasket(filename)
navigation.writeheader(basket, container)
basket.write(container)
if self.afterheader(container):
navigation.writefirstheader(basket)
for basket in self.baskets:
basket.process()
for basket in self.baskets:
basket.flush()
def afterheader(self, container):
"Find out if this is the header on the file."
return isinstance(container, LyXHeader)
def firstbasket(self):
"Create the first basket."
return self.addbasket(self.filename, self.writer)
def addbasket(self, filename, writer = None):
"Add a new basket."
if not writer:
writer = LineWriter(filename)
basket = SplitTOCBasket()
basket.setwriter(writer)
self.baskets.append(basket)
# set the page name everywhere
basket.page = filename
splitpartlink = SplitPartLink()
splitpartlink.page = os.path.basename(basket.page)
basket.processors = [splitpartlink]
return basket
def mustsplit(self, container):
"Find out if the oputput file has to be split at this entry."
if not container.partkey:
return False
if not container.partkey.filename:
return False
return True
def getfilename(self, container):
"Get the new file name for a given container."
partname = container.partkey.filename
base, extension = os.path.splitext(self.filename)
return base + '-' + partname + extension
class PostFormula(object):
"Postprocess a formula"
processedclass = Formula
def postprocess(self, last, formula, next):
"Postprocess any formulae"
if Options.jsmath or Options.mathjax:
return formula
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
functions = formula.searchremove(LabelFunction)
if len(functions) == 0:
label = self.createlabel(formula)
elif len(functions) == 1:
label = self.createlabel(formula, functions[0])
if len(functions) <= 1:
label.parent = formula
formula.contents.insert(0, label)
return
for function in functions:
label = self.createlabel(formula, function)
row = self.searchrow(function)
label.parent = row
row.contents.insert(0, label)
def createlabel(self, formula, function = None):
"Create a new label for a formula."
"Add a label to a formula."
number = NumberGenerator.instance.generatechaptered('formula')
partkey = PartKey().createformula(number)
if not formula.partkey:
formula.partkey = partkey
if not function:
label = Label()
label.create(partkey.tocentry + ' ', 'eq-' + number, type="eqnumber")
else:
label = function.label
label.complete(partkey.tocentry + ' ')
return label
def searchrow(self, function):
"Search for the row that contains the label function."
if isinstance(function.parent, Formula) or isinstance(function.parent, FormulaRow):
return function.parent
return self.searchrow(function.parent)
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 SplitPartBasket()
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()
return self
def convert(self):
"Perform the conversion for the document"
try:
self.processcontents()
except (Exception):
version = '[eLyXer version ' + GeneralConfig.version['number']
version += ' (' + GeneralConfig.version['date'] + ') in '
version += Options.location + '] '
Trace.error(version)
Trace.error('Conversion failed at ' + self.reader.currentline())
raise
def processcontents(self):
"Parse the contents and write it by containers"
factory = ContainerFactory()
processor = Processor(self.filtering)
while not self.reader.finished():
container = factory.createcontainer(self.reader)
result = processor.process(container)
self.writecontainer(result)
result = processor.postprocess(None)
self.writecontainer(result)
if not self.filtering:
self.basket.finish()
def writecontainer(self, container):
"Write each container to the correct basket."
if not container:
return
includes = container.searchremove(IncludeInset)
self.basket.write(container)
# recursive processing for IncludeInset
for include in includes:
for element in include.contents:
self.basket.write(element)
def isgoodinclude(self, include):
"Check if an include should not be removed."
current = include.parent
while current:
if isinstance(current.output, EmptyOutput):
return False
current = current.parent
return True
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(container.filename)
if 'firstline' in container.lstparams:
reader.setstart(int(container.lstparams['firstline']))
if 'lastline' in container.lstparams:
reader.setend(int(container.lstparams['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()