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