Initial import

This commit is contained in:
Gaetan Delannay 2009-06-29 14:06:01 +02:00
commit 4043163fc4
427 changed files with 18387 additions and 0 deletions

82
pod/__init__.py Executable file
View file

@ -0,0 +1,82 @@
# ------------------------------------------------------------------------------
# Appy is a framework for building applications in the Python language.
# Copyright (C) 2007 Gaetan Delannay
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,USA.
# ------------------------------------------------------------------------------
import time
from appy.shared.utils import Traceback
# Some POD-specific constants --------------------------------------------------
XHTML_HEADINGS = ('h1', 'h2', 'h3', 'h4', 'h5', 'h6')
XHTML_LISTS = ('ol', 'ul')
XHTML_PARAGRAPH_TAGS = XHTML_HEADINGS + XHTML_LISTS + ('p',)
XHTML_PARAGRAPH_TAGS_NO_LISTS = XHTML_HEADINGS + ('p',)
XHTML_INNER_TAGS = ('b', 'i', 'u', 'em')
XHTML_UNSTYLABLE_TAGS = XHTML_LISTS + ('li', 'a')
XML_SPECIAL_CHARS = {'<': '&lt;', '>': '&gt;', '&': '&amp;', '"': '&quot;',
"'": '&apos;'}
# ------------------------------------------------------------------------------
class PodError(Exception):
def dumpTraceback(buffer, tb, textNs, removeFirstLine):
if removeFirstLine:
# This error came from an exception raised by pod. The text of the
# error may be very long, so we avoid having it as error cause +
# in the first line of the traceback.
linesToRemove = 3
else:
linesToRemove = 2
i = 0
for tLine in tb.splitlines():
i += 1
if i > linesToRemove:
buffer.write('<%s:p>' % textNs)
buffer.dumpContent(tLine)
buffer.write('</%s:p>' % textNs)
dumpTraceback = staticmethod(dumpTraceback)
def dump(buffer, message, withinElement=None, removeFirstLine=False, dumpTb=True):
'''Dumps the error p_message in p_buffer.'''
# Define some handful shortcuts
e = buffer.env
ns = e.namespaces
dcNs = e.ns(e.NS_DC)
officeNs = e.ns(e.NS_OFFICE)
textNs = e.ns(e.NS_TEXT)
if withinElement:
buffer.write('<%s>' % withinElement.OD.elem)
for subTag in withinElement.subTags:
buffer.write('<%s>' % subTag.elem)
buffer.write('<%s:annotation><%s:creator>POD</%s:creator>' \
'<%s:date>%s</%s:date><%s:p>' % \
(officeNs, dcNs, dcNs, dcNs,
time.strftime('%Y-%m-%dT%H:%M:%S'), dcNs, textNs))
buffer.dumpContent(message)
buffer.write('</%s:p>' % textNs)
if dumpTb:
# We don't dump the traceback if it is an expression error (it is
# already included in the error message)
PodError.dumpTraceback(buffer, Traceback.get(), textNs,
removeFirstLine)
buffer.write('</%s:annotation>' % officeNs)
if withinElement:
subTags = withinElement.subTags[:]
subTags.reverse()
for subTag in subTags:
buffer.write('</%s>' % subTag.elem)
buffer.write('</%s>' % withinElement.OD.elem)
dump = staticmethod(dump)
# ------------------------------------------------------------------------------

205
pod/actions.py Executable file
View file

@ -0,0 +1,205 @@
# ------------------------------------------------------------------------------
# Appy is a framework for building applications in the Python language.
# Copyright (C) 2007 Gaetan Delannay
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,USA.
# ------------------------------------------------------------------------------
from appy.pod import PodError
from appy.pod.elements import *
# ------------------------------------------------------------------------------
EVAL_ERROR = 'Error while evaluating expression "%s".'
FROM_EVAL_ERROR = 'Error while evaluating the expression "%s" defined in the ' \
'"from" part of a statement.'
WRONG_SEQ_TYPE = 'Expression "%s" is not iterable.'
TABLE_NOT_ONE_CELL = "The table you wanted to populate with '%s' " \
"can\'t be dumped with the '-' option because it has " \
"more than one cell in it."
# ------------------------------------------------------------------------------
class BufferAction:
'''Abstract class representing a action (=statement) that must be performed
on the content of a buffer (if, for...).'''
def __init__(self, name, buffer, expr, elem, minus, source, fromExpr):
self.name = name # Actions may be named. Currently, the name of an
# action is only used for giving a name to "if" actions; thanks to this
# name, "else" actions that are far away may reference their "if".
self.buffer = buffer # The object of the action
self.expr = expr # Python expression to evaluate (may be None in the
# case of a NullAction or ElseAction, for example)
self.elem = elem # The element within the buffer that is the object
# of the action.
self.minus = minus # If True, the main buffer element(s) must not be
# dumped.
self.result = self.buffer.getFileBuffer()
self.source = source # if 'buffer', we must dump the (evaluated) buffer
# content. If 'from', we must dump what comes from the 'from' part of
# the action (='fromExpr')
self.fromExpr = fromExpr
# We store the result of evaluation of expr and fromExpr
self.exprResult = None
self.fromExprResult = None
def writeError(self, errorMessage, dumpTb=True):
# Empty the buffer
self.buffer.__init__(self.buffer.env, self.buffer.parent)
PodError.dump(self.buffer, errorMessage, withinElement=self.elem,
dumpTb=dumpTb)
self.buffer.evaluate()
def execute(self):
# Check that if minus is set, we have an element which can accept it
if self.minus and isinstance(self.elem, Table) and \
(not self.elem.tableInfo.isOneCell()):
self.writeError(TABLE_NOT_ONE_CELL % self.expr)
else:
errorOccurred = False
if self.expr:
try:
self.exprResult = eval(self.expr, self.buffer.env.context)
except:
self.exprResult = None
self.writeError(EVAL_ERROR % self.expr)
errorOccurred = True
if not errorOccurred:
self.do()
def evaluateBuffer(self):
if self.source == 'buffer':
self.buffer.evaluate(removeMainElems = self.minus)
else:
# Evaluate fromExpr
self.fromExprResult = None
errorOccurred = False
try:
self.fromExprResult= eval(self.fromExpr,self.buffer.env.context)
except PodError, pe:
self.writeError(FROM_EVAL_ERROR % self.fromExpr + ' ' + str(pe),
dumpTb=False)
errorOccurred = True
except:
self.writeError(FROM_EVAL_ERROR % self.fromExpr)
errorOccurred = True
if not errorOccurred:
self.result.write(self.fromExprResult)
class IfAction(BufferAction):
'''Action that determines if we must include the content of the buffer in
the result or not.'''
def do(self):
if self.exprResult:
self.evaluateBuffer()
else:
if self.buffer.isMainElement(Cell.OD):
# Don't leave the current row with a wrong number of cells
self.result.dumpElement(Cell.OD.elem)
class ElseAction(IfAction):
'''Action that is linked to a previous "if" action. In fact, an "else"
action works exactly like an "if" action, excepted that instead of
defining a conditional expression, it is based on the negation of the
conditional expression of the last defined "if" action.'''
def __init__(self, name, buffer, expr, elem, minus, source, fromExpr,
ifAction):
IfAction.__init__(self, name, buffer, None, elem, minus, source,
fromExpr)
self.ifAction = ifAction
def do(self):
# The result of this "else" action is "not <result from last execution
# of linked 'if' action>".
self.exprResult = not self.ifAction.exprResult
IfAction.do(self)
class ForAction(BufferAction):
'''Actions that will include the content of the buffer as many times as
specified by the action parameters.'''
def __init__(self, name, buffer, expr, elem, minus, iter, source, fromExpr):
BufferAction.__init__(self, name, buffer, expr, elem, minus, source,
fromExpr)
self.iter = iter # Name of the iterator variable used in the each loop
def do(self):
context = self.buffer.env.context
# Check self.exprResult type
try:
iter(self.exprResult)
# All "iterable" objects are OK. Thanks to Bernhard Bender for this
# improvement.
except TypeError:
self.writeError(WRONG_SEQ_TYPE % self.expr)
return
# Remember variable hidden by iter if any
hasHiddenVariable = False
if context.has_key(self.iter):
hiddenVariable = context[self.iter]
hasHiddenVariable = True
# In the case of cells, initialize some values
isCell = False
if isinstance(self.elem, Cell):
isCell = True
nbOfColumns = self.elem.tableInfo.nbOfColumns
initialColIndex = self.elem.tableInfo.curColIndex
currentColIndex = initialColIndex
rowAttributes = self.elem.tableInfo.curRowAttrs
# If self.exprResult is empty, dump an empty cell to avoid
# having the wrong number of cells for the current row
if not self.exprResult:
self.result.dumpElement(Cell.OD.elem)
# Enter the "for" loop
for item in self.exprResult:
context[self.iter] = item
# Cell: add a new row if we are at the end of a row
if isCell and (currentColIndex == nbOfColumns):
self.result.dumpEndElement(Row.OD.elem)
self.result.dumpStartElement(Row.OD.elem, rowAttributes)
currentColIndex = 0
self.evaluateBuffer()
# Cell: increment the current column index
if isCell:
currentColIndex += 1
# Cell: leave the last row with the correct number of cells
if isCell and self.exprResult:
wrongNbOfCells = (currentColIndex-1) - initialColIndex
if wrongNbOfCells < 0: # Too few cells for last row
for i in range(abs(wrongNbOfCells)):
context[self.iter] = ''
self.buffer.evaluate(subElements=False)
# This way, the cell is dumped with the correct styles
elif wrongNbOfCells > 0: # Too many cells for last row
# Finish current row
nbOfMissingCells = 0
if currentColIndex < nbOfColumns:
nbOfMissingCells = nbOfColumns - currentColIndex
context[self.iter] = ''
for i in range(nbOfMissingCells):
self.buffer.evaluate(subElements=False)
self.result.dumpEndElement(Row.OD.elem)
# Create additional row with remaining cells
self.result.dumpStartElement(Row.OD.elem, rowAttributes)
nbOfRemainingCells = wrongNbOfCells + nbOfMissingCells
nbOfMissingCellsLastLine = nbOfColumns - nbOfRemainingCells
context[self.iter] = ''
for i in range(nbOfMissingCellsLastLine):
self.buffer.evaluate(subElements=False)
# Restore hidden variable if any
if hasHiddenVariable:
context[self.iter] = hiddenVariable
else:
if self.exprResult:
del context[self.iter]
class NullAction(BufferAction):
'''Action that does nothing. Used in conjunction with a "from" clause, it
allows to insert in a buffer arbitrary odt content.'''
def do(self):
self.evaluateBuffer()
# ------------------------------------------------------------------------------

501
pod/buffers.py Executable file
View file

@ -0,0 +1,501 @@
# ------------------------------------------------------------------------------
# Appy is a framework for building applications in the Python language.
# Copyright (C) 2007 Gaetan Delannay
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,USA.
# ------------------------------------------------------------------------------
import re
from appy.pod import PodError, XML_SPECIAL_CHARS
from appy.pod.elements import *
from appy.pod.actions import IfAction, ElseAction, ForAction, NullAction
# ------------------------------------------------------------------------------
class ParsingError(Exception): pass
# ParsingError-related constants -----------------------------------------------
ELEMENT = 'identifies the part of the document that will be impacted ' \
'by the command. It must be one of %s.' % str(PodElement.POD_ELEMS)
FOR_EXPRESSION = 'must be of the form: {name} in {expression}. {name} must be '\
'a Python variable name. It is the name of the iteration ' \
'variable. {expression} is a Python expression that, when ' \
'evaluated, produces a Python sequence (tuple, string, list, '\
'etc).'
POD_STATEMENT = 'A Pod statement has the ' \
'form: do {element} [{command} {expression}]. {element} ' + \
ELEMENT + ' Optional {command} can be "if" ' \
'(conditional inclusion of the element) or "for" (multiple ' \
'inclusion of the element). For an "if" command, {expression} '\
'is any Python expression. For a "for" command, {expression} '+\
FOR_EXPRESSION
FROM_CLAUSE = 'A "from" clause has the form: from {expression}, where ' \
'{expression} is a Python expression that, when evaluated, ' \
'produces a valid chunk of odt content that will be inserted ' \
'instead of the element that is the target of the note.'
BAD_STATEMENT_GROUP = 'Syntax error while parsing a note whose content is ' \
'"%s". In a note, you may specify at most 2 lines: a ' \
'pod statement and a "from" clause. ' + POD_STATEMENT + \
' ' + FROM_CLAUSE
BAD_STATEMENT = 'Syntax error for statement "%s". ' + POD_STATEMENT
BAD_ELEMENT = 'Bad element "%s". An element ' + ELEMENT
BAD_MINUS = "The '-' operator can't be used with element '%s'. It can only be "\
"specified for elements among %s."
ELEMENT_NOT_FOUND = 'Action specified element "%s" but available elements ' \
'in this part of the document are %s.'
BAD_FROM_CLAUSE = 'Syntax error in "from" clause "%s". ' + FROM_CLAUSE
DUPLICATE_NAMED_IF = 'An "if" statement with the same name already exists.'
ELSE_WITHOUT_IF = 'No previous "if" statement could be found for this "else" ' \
'statement.'
ELSE_WITHOUT_NAMED_IF = 'I could not find an "if" statement named "%s".'
BAD_FOR_EXPRESSION = 'Bad "for" expression "%s". A "for" expression ' + \
FOR_EXPRESSION
EVAL_EXPR_ERROR = 'Error while evaluating expression "%s". %s'
NULL_ACTION_ERROR = 'There was a problem with this action. Possible causes: ' \
'(1) you specified no action (ie "do text") while not ' \
'specifying any from clause; (2) you specified the from ' \
'clause on the same line as the action, which is not ' \
'allowed (ie "do text from ...").'
# ------------------------------------------------------------------------------
class BufferIterator:
def __init__(self, buffer):
self.buffer = buffer
self.remainingSubBufferIndexes = self.buffer.subBuffers.keys()
self.remainingElemIndexes = self.buffer.elements.keys()
self.remainingSubBufferIndexes.sort()
self.remainingElemIndexes.sort()
def hasNext(self):
return self.remainingSubBufferIndexes or self.remainingElemIndexes
def next(self):
nextSubBufferIndex = None
if self.remainingSubBufferIndexes:
nextSubBufferIndex = self.remainingSubBufferIndexes[0]
nextExprIndex = None
if self.remainingElemIndexes:
nextExprIndex = self.remainingElemIndexes[0]
# Compute min between nextSubBufferIndex and nextExprIndex
if (nextSubBufferIndex != None) and (nextExprIndex != None):
res = min(nextSubBufferIndex, nextExprIndex)
elif (nextSubBufferIndex == None) and (nextExprIndex != None):
res = nextExprIndex
elif (nextSubBufferIndex != None) and (nextExprIndex == None):
res = nextSubBufferIndex
# Update "remaining" lists
if res == nextSubBufferIndex:
self.remainingSubBufferIndexes = self.remainingSubBufferIndexes[1:]
resDict = self.buffer.subBuffers
elif res == nextExprIndex:
self.remainingElemIndexes = self.remainingElemIndexes[1:]
resDict = self.buffer.elements
return res, resDict[res]
# ------------------------------------------------------------------------------
class Buffer:
'''Abstract class representing any buffer used during rendering.'''
elementRex = re.compile('([\w-]+:[\w-]+)\s*(.*?)>', re.S)
def __init__(self, env, parent):
self.parent = parent
self.subBuffers = {} # ~{i_bufferIndex: Buffer}~
self.env = env
def addSubBuffer(self, subBuffer=None):
if not subBuffer:
subBuffer = MemoryBuffer(self.env, self)
self.subBuffers[self.getLength()] = subBuffer
subBuffer.parent = self
return subBuffer
def removeLastSubBuffer(self):
subBufferIndexes = self.subBuffers.keys()
subBufferIndexes.sort()
lastIndex = subBufferIndexes.pop()
del self.subBuffers[lastIndex]
def write(self, something): pass # To be overridden
def getLength(self): pass # To be overridden
def dumpStartElement(self, elem, attrs={}):
self.write('<%s' % elem)
for name, value in attrs.items():
self.write(' %s="%s"' % (name, value))
self.write('>')
def dumpEndElement(self, elem):
self.write('</%s>' % elem)
def dumpElement(self, elem, content=None, attrs={}):
'''For dumping a whole element at once.'''
self.dumpStartElement(elem, attrs)
if content:
self.dumpContent(content)
self.dumpEndElement(elem)
def dumpContent(self, content):
'''Dumps string p_content into the buffer.'''
for c in content:
if XML_SPECIAL_CHARS.has_key(c):
self.write(XML_SPECIAL_CHARS[c])
else:
self.write(c)
def dumpAttribute(self, name, value):
self.write(''' %s="%s" ''' % (name, value))
# ------------------------------------------------------------------------------
class FileBuffer(Buffer):
def __init__(self, env, result):
Buffer.__init__(self, env, None)
self.result = result
self.content = file(result, 'w')
self.content.write('<?xml version="1.0" encoding="UTF-8"?>')
def getLength(self): return 0
# getLength is used to manage insertions into sub-buffers. But in the case
# of a FileBuffer, we will only have 1 sub-buffer at a time, and we don't
# care about where it will be inserted into the FileBuffer.
def write(self, something):
self.content.write(something.encode('utf-8'))
def addExpression(self, expression):
try:
self.dumpContent(Expression(expression).evaluate(self.env.context))
except Exception, e:
PodError.dump(self, EVAL_EXPR_ERROR % (expression, e), dumpTb=False)
def pushSubBuffer(self, subBuffer): pass
# ------------------------------------------------------------------------------
class MemoryBuffer(Buffer):
actionRex = re.compile('(?:(\w+)\s*\:\s*)?do\s+(\w+)(-)?' \
'(?:\s+(for|if|else)\s*(.*))?')
forRex = re.compile('\s*([\w\-_]+)\s+in\s+(.*)')
def __init__(self, env, parent):
Buffer.__init__(self, env, parent)
self.content = u''
self.elements = {}
self.action = None
def addSubBuffer(self, subBuffer=None):
sb = Buffer.addSubBuffer(self, subBuffer)
self.content += ' ' # To avoid having several subbuffers referenced at
# the same place within this buffer.
return sb
def getFileBuffer(self):
if isinstance(self.parent, FileBuffer):
res = self.parent
else:
res = self.parent.getFileBuffer()
return res
def getLength(self): return len(self.content)
def write(self, thing): self.content += thing
def getIndex(self, podElemName):
res = -1
for index, podElem in self.elements.iteritems():
if podElem.__class__.__name__.lower() == podElemName:
if index > res:
res = index
return res
def getMainElement(self):
res = None
if self.elements.has_key(0):
res = self.elements[0]
return res
def isMainElement(self, elem):
res = False
mainElem = self.getMainElement()
if mainElem and (elem == mainElem.OD.elem):
res = True
# Check if this element is not found again within the buffer
for index, podElem in self.elements.iteritems():
if podElem.OD:
if (podElem.OD.elem == mainElem.OD.elem) and (index != 0):
res = False
break
return res
def unreferenceElement(self, elem):
# Find last occurrence of this element
elemIndex = -1
for index, podElem in self.elements.iteritems():
if podElem.OD:
if (podElem.OD.elem == elem) and (index > elemIndex):
elemIndex = index
del self.elements[elemIndex]
def pushSubBuffer(self, subBuffer):
'''Sets p_subBuffer at the very end of the buffer.'''
subIndex = None
for index, aSubBuffer in self.subBuffers.iteritems():
if aSubBuffer == subBuffer:
subIndex = index
break
if subIndex != None:
# Indeed, it is possible that this buffer is not referenced
# in the parent (if it is a temp buffer generated from a cut)
del self.subBuffers[subIndex]
self.subBuffers[self.getLength()] = subBuffer
self.content += u' '
def transferAllContent(self):
'''Transfer all content to parent.'''
if isinstance(self.parent, FileBuffer):
# First unreference all elements
for index in self.getElementIndexes(expressions=False):
del self.elements[index]
self.evaluate()
else:
# Transfer content in itself
oldParentLength = self.parent.getLength()
self.parent.write(self.content)
# Transfer elements
for index, podElem in self.elements.iteritems():
self.parent.elements[oldParentLength + index] = podElem
# Transfer subBuffers
for index, buf in self.subBuffers.iteritems():
self.parent.subBuffers[oldParentLength + index] = buf
# Empty the buffer
MemoryBuffer.__init__(self, self.env, self.parent)
# Change buffer position wrt parent
self.parent.pushSubBuffer(self)
def addElement(self, elem):
newElem = PodElement.create(elem)
self.elements[self.getLength()] = newElem
if isinstance(newElem, Cell) or isinstance(newElem, Table):
newElem.tableInfo = self.env.getTable()
def addExpression(self, expression):
# Create the POD expression
expr = Expression(expression)
expr.expr = expression
self.elements[self.getLength()] = expr
self.content += u' ' # To be sure that an expr and an elem can't be found
# at the same index in the buffer.
def createAction(self, statementGroup):
'''Tries to create an action based on p_statementGroup. If the statement
is not correct, r_ is -1. Else, r_ is the index of the element within
the buffer that is the object of the action.'''
res = -1
try:
# Check the whole statement group
if not statementGroup or (len(statementGroup) > 2):
raise ParsingError(BAD_STATEMENT_GROUP % str(statementGroup))
# Check the statement
statement = statementGroup[0]
aRes = self.actionRex.match(statement)
if not aRes:
raise ParsingError(BAD_STATEMENT % statement)
statementName, podElem, minus, actionType, subExpr = aRes.groups()
if not (podElem in PodElement.POD_ELEMS):
raise ParsingError(BAD_ELEMENT % podElem)
if minus and (not podElem in PodElement.MINUS_ELEMS):
raise ParsingError(
BAD_MINUS % (podElem, PodElement.MINUS_ELEMS))
indexPodElem = self.getIndex(podElem)
if indexPodElem == -1:
raise ParsingError(
ELEMENT_NOT_FOUND % (podElem, str([
e.__class__.__name__.lower() \
for e in self.elements.values()])))
podElem = self.elements[indexPodElem]
# Check the 'from' clause
fromClause = None
source = 'buffer'
if len(statementGroup) > 1:
fromClause = statementGroup[1]
source = 'from'
if not fromClause.startswith('from '):
raise ParsingError(BAD_FROM_CLAUSE % fromClause)
fromClause = fromClause[5:]
# Create the action
if actionType == 'if':
self.action = IfAction(statementName, self, subExpr, podElem,
minus, source, fromClause)
self.env.ifActions.append(self.action)
if self.action.name:
# We must register this action as a named action
if self.env.namedIfActions.has_key(self.action.name):
raise ParsingError(DUPLICATE_NAMED_IF)
self.env.namedIfActions[self.action.name] = self.action
elif actionType == 'else':
if not self.env.ifActions:
raise ParsingError(ELSE_WITHOUT_IF)
# Does the "else" action reference a named "if" action?
ifReference = subExpr.strip()
if ifReference:
if not self.env.namedIfActions.has_key(ifReference):
raise ParsingError(ELSE_WITHOUT_NAMED_IF % ifReference)
linkedIfAction = self.env.namedIfActions[ifReference]
# This "else" action "consumes" the "if" action: this way,
# it is not possible to define two "else" actions related to
# the same "if".
del self.env.namedIfActions[ifReference]
self.env.ifActions.remove(linkedIfAction)
else:
linkedIfAction = self.env.ifActions.pop()
self.action = ElseAction(statementName, self, None, podElem,
minus, source, fromClause,
linkedIfAction)
elif actionType == 'for':
forRes = MemoryBuffer.forRex.match(subExpr.strip())
if not forRes:
raise ParsingError(BAD_FOR_EXPRESSION % subExpr)
iter, subExpr = forRes.groups()
self.action = ForAction(statementName, self, subExpr, podElem,
minus, iter, source, fromClause)
else: # null action
if not fromClause:
raise ParsingError(NULL_ACTION_ERROR)
self.action = NullAction(statementName, self, None, podElem,
None, source, fromClause)
res = indexPodElem
except ParsingError, ppe:
PodError.dump(self, ppe, removeFirstLine=True)
return res
def cut(self, index, keepFirstPart):
'''Cuts this buffer into 2 parts. Depending on p_keepFirstPart, the 1st
(from 0 to index-1) or the second (from index to the end) part of the
buffer is returned as a MemoryBuffer instance without parent; the other
part is self.'''
res = MemoryBuffer(self.env, None)
# Manage buffer meta-info (elements, expressions, subbuffers)
iter = BufferIterator(self)
subBuffersToDelete = []
elementsToDelete = []
mustShift = False
while iter.hasNext():
itemIndex, item = iter.next()
if keepFirstPart:
if itemIndex >= index:
newIndex = itemIndex-index
if isinstance(item, MemoryBuffer):
res.subBuffers[newIndex] = item
subBuffersToDelete.append(itemIndex)
else:
res.elements[newIndex] = item
elementsToDelete.append(itemIndex)
else:
if itemIndex < index:
if isinstance(item, MemoryBuffer):
res.subBuffers[itemIndex] = item
subBuffersToDelete.append(itemIndex)
else:
res.elements[itemIndex] = item
elementsToDelete.append(itemIndex)
else:
mustShift = True
if elementsToDelete:
for elemIndex in elementsToDelete:
del self.elements[elemIndex]
if subBuffersToDelete:
for subIndex in subBuffersToDelete:
del self.subBuffers[subIndex]
if mustShift:
elements = {}
for elemIndex, elem in self.elements.iteritems():
elements[elemIndex-index] = elem
self.elements = elements
subBuffers = {}
for subIndex, buf in self.subBuffers.iteritems():
subBuffers[subIndex-index] = buf
self.subBuffers = subBuffers
# Manage content
if keepFirstPart:
res.write(self.content[index:])
self.content = self.content[:index]
else:
res.write(self.content[:index])
self.content = self.content[index:]
return res
def getElementIndexes(self, expressions=True):
res = []
for index, elem in self.elements.iteritems():
condition = isinstance(elem, Expression)
if not expressions:
condition = not condition
if condition:
res.append(index)
return res
def transferActionIndependentContent(self, actionElemIndex):
# Manage content to transfer to parent buffer
if actionElemIndex != 0:
actionIndependentBuffer = self.cut(actionElemIndex,
keepFirstPart=False)
actionIndependentBuffer.parent = self.parent
actionIndependentBuffer.transferAllContent()
self.parent.pushSubBuffer(self)
# Manage content to transfer to a child buffer
actionElemIndex = self.getIndex(
self.action.elem.__class__.__name__.lower())
# We recompute actionElemIndex because after cut it may have changed
elemIndexes = self.getElementIndexes(expressions=False)
elemIndexes.sort()
if elemIndexes.index(actionElemIndex) != (len(elemIndexes)-1):
# I must create a sub-buffer with the impactable elements after
# the action-related element
childBuffer = self.cut(elemIndexes[elemIndexes.index(
actionElemIndex)+1], keepFirstPart=True)
self.addSubBuffer(childBuffer)
res = childBuffer
else:
res = self
return res
def getStartIndex(self, removeMainElems):
'''When I must dump the buffer, sometimes (if p_removeMainElems is
True), I must dump only a subset of it. This method returns the start
index of the buffer part I must dump.'''
if removeMainElems:
# Find the start position of the deepest element to remove
deepestElem = self.action.elem.DEEPEST_TO_REMOVE
pos = self.content.find('<%s' % deepestElem.elem)
pos = pos + len(deepestElem.elem)
# Now we must find the position of the end of this start tag,
# skipping potential attributes.
inAttrValue = False # Are we parsing an attribute value ?
endTagFound = False # Have we found the end of this tag ?
while not endTagFound:
pos += 1
nextChar = self.content[pos]
if (nextChar == '>') and not inAttrValue:
# Yes we have it
endTagFound = True
elif nextChar == '"':
inAttrValue = not inAttrValue
res = pos + 1
else:
res = 0
return res
def getStopIndex(self, removeMainElems):
'''This method returns the stop index of the buffer part I must dump.'''
if removeMainElems:
ns = self.env.namespaces
deepestElem = self.action.elem.DEEPEST_TO_REMOVE
pos = self.content.rfind('</%s>' % deepestElem.getFullName(ns))
res = pos
else:
res = self.getLength()
return res
def evaluate(self, subElements=True, removeMainElems=False):
#print 'Evaluating buffer', self.content.encode('utf-8'), self.elements
result = self.getFileBuffer()
if not subElements:
result.write(self.content)
else:
iter = BufferIterator(self)
currentIndex = self.getStartIndex(removeMainElems)
while iter.hasNext():
index, evalEntry = iter.next()
result.write(self.content[currentIndex:index])
currentIndex = index + 1
if isinstance(evalEntry, Expression):
try:
result.dumpContent(evalEntry.evaluate(self.env.context))
except Exception, e:
PodError.dump(result, EVAL_EXPR_ERROR % (
evalEntry.expr, e), dumpTb=False)
else: # It is a subBuffer
#print '******Subbuffer*************'
# This is a bug.
if evalEntry.action:
evalEntry.action.execute()
else:
result.write(evalEntry.content)
stopIndex = self.getStopIndex(removeMainElems)
if currentIndex < (stopIndex-1):
result.write(self.content[currentIndex:stopIndex])
# ------------------------------------------------------------------------------

222
pod/converter.py Executable file
View file

@ -0,0 +1,222 @@
# ------------------------------------------------------------------------------
# Appy is a framework for building applications in the Python language.
# Copyright (C) 2007 Gaetan Delannay
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,USA.
# ------------------------------------------------------------------------------
import sys, os, os.path, time, signal
from optparse import OptionParser
ODT_FILE_TYPES = {'doc': 'MS Word 97', # Could be 'MS Word 2003 XML'
'pdf': 'writer_pdf_Export',
'rtf': 'Rich Text Format',
'txt': 'Text',
'html': 'HTML (StarWriter)',
'htm': 'HTML (StarWriter)',
'odt': 'ODT'}
# Conversion to ODT does not make any conversion; it simply updates indexes and
# linked documents.
# ------------------------------------------------------------------------------
class ConverterError(Exception): pass
# ConverterError-related messages ----------------------------------------------
DOC_NOT_FOUND = 'Document "%s" was not found.'
URL_NOT_FOUND = 'Doc URL "%s" is wrong. %s'
BAD_RESULT_TYPE = 'Bad result type "%s". Available types are %s.'
CANNOT_WRITE_RESULT = 'I cannot write result "%s". %s'
CONNECT_ERROR = 'Could not connect to OpenOffice on port %d. UNO ' \
'(OpenOffice API) says: %s.'
# Some constants ---------------------------------------------------------------
DEFAULT_PORT = 2002
# ------------------------------------------------------------------------------
class Converter:
'''Converts an ODT document into pdf, doc, txt or rtf.'''
exeVariants = ('soffice.exe', 'soffice')
pathReplacements = {'program files': 'progra~1',
'openoffice.org 1': 'openof~1',
'openoffice.org 2': 'openof~1',
}
def __init__(self, docPath, resultType, port=DEFAULT_PORT):
self.port = port
self.docUrl = self.getDocUrl(docPath)
self.resultFilter = self.getResultFilter(resultType)
self.resultUrl = self.getResultUrl(resultType)
self.ooContext = None
self.oo = None # OpenOffice application object
self.doc = None # OpenOffice loaded document
def getDocUrl(self, docPath):
if not os.path.exists(docPath) and not os.path.isfile(docPath):
raise ConverterError(DOC_NOT_FOUND % docPath)
docAbsPath = os.path.abspath(docPath)
docUrl = 'file:///' + docAbsPath.replace('\\', '/')
return docUrl
def getResultFilter(self, resultType):
if ODT_FILE_TYPES.has_key(resultType):
res = ODT_FILE_TYPES[resultType]
else:
raise ConverterError(BAD_RESULT_TYPE % (resultType,
ODT_FILE_TYPES.keys()))
return res
def getResultUrl(self, resultType):
baseName = os.path.splitext(self.docUrl)[0]
if resultType != 'odt':
res = '%s.%s' % (baseName, resultType)
else:
res = '%s.res.%s' % (baseName, resultType)
fileName = res[8:]
try:
f = open(fileName, 'w')
f.write('Hello')
f.close()
os.remove(fileName)
return res
except OSError, oe:
raise ConverterError(CANNOT_WRITE_RESULT % (res, oe))
def connect(self):
'''Connects to OpenOffice'''
import socket
import uno
from com.sun.star.connection import NoConnectException
try:
# Get the uno component context from the PyUNO runtime
localContext = uno.getComponentContext()
# Create the UnoUrlResolver
resolver = localContext.ServiceManager.createInstanceWithContext(
"com.sun.star.bridge.UnoUrlResolver", localContext)
# Connect to the running office
self.ooContext = resolver.resolve(
'uno:socket,host=localhost,port=%d;urp;StarOffice.' \
'ComponentContext' % self.port)
# Is seems that we can't define a timeout for this method.
# I need it because, for example, when a web server already listens
# to the given port (thus, not a OpenOffice instance), this method
# blocks.
smgr = self.ooContext.ServiceManager
# Get the central desktop object
self.oo = smgr.createInstanceWithContext(
'com.sun.star.frame.Desktop', self.ooContext)
except NoConnectException, nce:
raise ConverterError(CONNECT_ERROR % (self.port, nce))
def disconnect(self):
self.doc.close(True)
# Do a nasty thing before exiting the python process. In case the
# last call is a oneway call (e.g. see idl-spec of insertString),
# it must be forced out of the remote-bridge caches before python
# exits the process. Otherwise, the oneway call may or may not reach
# the target object.
# I do this here by calling a cheap synchronous call (getPropertyValue).
self.ooContext.ServiceManager
def loadDocument(self):
from com.sun.star.lang import IllegalArgumentException, \
IndexOutOfBoundsException
# I need to use IndexOutOfBoundsException because sometimes, when
# using sections.getCount, UNO returns a number that is bigger than
# the real number of sections (this is because it also counts the
# sections that are present within the sub-documents to integrate)
from com.sun.star.beans import PropertyValue
try:
# Load the document to convert in a new hidden frame
prop = PropertyValue()
prop.Name = 'Hidden'
prop.Value = True
self.doc = self.oo.loadComponentFromURL(self.docUrl, "_blank", 0,
(prop,))
# Update all indexes
indexes = self.doc.getDocumentIndexes()
indexesCount = indexes.getCount()
if indexesCount != 0:
for i in range(indexesCount):
try:
indexes.getByIndex(i).update()
except IndexOutOfBoundsException:
pass
# Update sections
self.doc.updateLinks()
sections = self.doc.getTextSections()
sectionsCount = sections.getCount()
if sectionsCount != 0:
for i in range(sectionsCount-1, -1, -1):
# I must walk into the section from last one to the first
# one. Else, when "disposing" sections, I remove sections
# and the remaining sections other indexes.
try:
section = sections.getByIndex(i)
if section.FileLink and section.FileLink.FileURL:
section.dispose() # This method removes the
# <section></section> tags without removing the content
# of the section. Else, it won't appear.
except IndexOutOfBoundsException:
pass
except IllegalArgumentException, iae:
raise ConverterError(URL_NOT_FOUND % (self.docUrl, iae))
def convertDocument(self):
if self.resultFilter != 'ODT':
# I must really perform a conversion
from com.sun.star.beans import PropertyValue
prop = PropertyValue()
prop.Name = 'FilterName'
prop.Value = self.resultFilter
self.doc.storeToURL(self.resultUrl, (prop,))
else:
self.doc.storeToURL(self.resultUrl, ())
def run(self):
self.connect()
self.loadDocument()
self.convertDocument()
self.disconnect()
# ConverterScript-related messages ---------------------------------------------
WRONG_NB_OF_ARGS = 'Wrong number of arguments.'
ERROR_CODE = 1
# Class representing the command-line program ----------------------------------
class ConverterScript:
usage = 'usage: python converter.py fileToConvert outputType [options]\n' \
' where fileToConvert is the absolute or relative pathname of\n' \
' the ODT file you want to convert;\n'\
' and outputType is the output format, that must be one of\n' \
' %s.\n' \
' "python" should be a UNO-enabled Python interpreter (ie the one\n' \
' which is included in the OpenOffice.org distribution).' % \
str(ODT_FILE_TYPES.keys())
def run(self):
optParser = OptionParser(usage=ConverterScript.usage)
optParser.add_option("-p", "--port", dest="port",
help="The port on which OpenOffice runs " \
"Default is %d." % DEFAULT_PORT,
default=DEFAULT_PORT, metavar="PORT", type='int')
(options, args) = optParser.parse_args()
if len(args) != 2:
sys.stderr.write(WRONG_NB_OF_ARGS)
sys.stderr.write('\n')
optParser.print_help()
sys.exit(ERROR_CODE)
converter = Converter(args[0], args[1], options.port)
try:
converter.run()
except ConverterError, ce:
sys.stderr.write(str(ce))
sys.stderr.write('\n')
optParser.print_help()
sys.exit(ERROR_CODE)
# ------------------------------------------------------------------------------
if __name__ == '__main__':
ConverterScript().run()
# ------------------------------------------------------------------------------

230
pod/doc_importers.py Executable file
View file

@ -0,0 +1,230 @@
# ------------------------------------------------------------------------------
# Appy is a framework for building applications in the Python language.
# Copyright (C) 2007 Gaetan Delannay
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,USA.
# ------------------------------------------------------------------------------
import os, os.path, time, shutil, struct
from appy.pod import PodError
from appy.pod.odf_parser import OdfEnvironment
# ------------------------------------------------------------------------------
FILE_NOT_FOUND = "'%s' does not exist or is not a file."
PDF_TO_IMG_ERROR = 'A PDF file could not be converted into images. Please ' \
'ensure that Ghostscript (gs) is installed on your ' \
'system and the "gs" program is in the path.'
# ------------------------------------------------------------------------------
class DocImporter:
'''Base class used for importing external content into a pod template (an
image, another pod template, another odt document...'''
def __init__(self, content, at, format, tempFolder, ns):
self.content = content
self.at = at # If content is None, p_at tells us where to find it
# (file system path, url, etc)
self.format = format
self.res = u''
self.ns = ns
# Unpack some useful namespaces
self.textNs = ns[OdfEnvironment.NS_TEXT]
self.linkNs = ns[OdfEnvironment.NS_XLINK]
self.drawNs = ns[OdfEnvironment.NS_DRAW]
self.svgNs = ns[OdfEnvironment.NS_SVG]
self.tempFolder = tempFolder
self.importFolder = self.getImportFolder()
if self.at:
# Check that the file exists
if not os.path.isfile(self.at):
raise PodError(FILE_NOT_FOUND % self.at)
self.importPath = self.moveFile(self.at)
else:
# We need to dump the file content (in self.content) in a temp file
# first. self.content may be binary or a file handler.
if not os.path.exists(self.importFolder):
os.mkdir(self.importFolder)
if isinstance(self.content, file):
self.fileName = os.path.basename(self.content.name)
fileContent = self.content.read()
else:
self.fileName = 'f%f.%s' % (time.time(), self.format)
fileContent = self.content
self.importPath = self.getImportPath(self.fileName)
theFile = file(self.importPath, 'w')
theFile.write(fileContent)
theFile.close()
self.importPath = os.path.abspath(self.importPath)
def getImportFolder(self):
'''This method must be overridden and gives the path where to dump the
content of the document or image. In the case of a document it is a
temp folder; in the case of an image it is a folder within the ODT
result.'''
pass
def getImportPath(self, fileName):
'''Import path is the path to the external file or image that is now
stored on disk. We check here that this name does not correspond
to an existing file; if yes, we change the path until we get a path
that does not correspond to an existing file.'''
res = '%s/%s' % (self.importFolder, fileName)
resIsGood = False
while not resIsGood:
if not os.path.exists(res):
resIsGood = True
else:
# We must find another file name, this one already exists.
name, ext = os.path.splitext(res)
name += 'g'
res = name + ext
return res
def moveFile(self, at):
'''In the case parameter "at" was used, we may want to move the file at
p_at within the ODT result (for images) or do nothing (for
documents).'''
return at
class OdtImporter(DocImporter):
'''This class allows to import the content of another ODT document into a
pod template.'''
def getImportFolder(self):
return '%s/docImports' % self.tempFolder
def run(self):
self.res += '<%s:section %s:name="PodImportSection%f">' \
'<%s:section-source %s:href="%s" ' \
'%s:filter-name="writer8"/></%s:section>' % (
self.textNs, self.textNs, time.time(), self.textNs,
self.linkNs, self.importPath, self.textNs, self.textNs)
return self.res
class PdfImporter(DocImporter):
'''This class allows to import the content of a PDF file into a pod
template. It calls gs to split the PDF into images and calls the
ImageImporter for importing it into the result.'''
imagePrefix = 'PdfPart'
def getImportFolder(self):
return '%s/docImports' % self.tempFolder
def run(self):
# Split the PDF into images with Ghostscript
imagesFolder = os.path.dirname(self.importPath)
cmd = 'gs -dNOPAUSE -dBATCH -sDEVICE=jpeg -r125x125 ' \
'-sOutputFile=%s/%s%%d.jpg %s' % \
(imagesFolder, self.imagePrefix, self.importPath)
os.system(cmd)
# Check that at least one image was generated
succeeded = False
firstImage = '%s1.jpg' % self.imagePrefix
for fileName in os.listdir(imagesFolder):
if fileName == firstImage:
succeeded = True
break
if not succeeded:
raise PodError(PDF_TO_IMG_ERROR)
# Insert images into the result.
noMoreImages = False
i = 0
while not noMoreImages:
i += 1
nextImage = '%s/%s%d.jpg' % (imagesFolder, self.imagePrefix, i)
if os.path.exists(nextImage):
# Use internally an Image importer for doing this job.
imgImporter = ImageImporter(None, nextImage, 'jpg',
self.tempFolder, self.ns)
imgImporter.setAnchor('paragraph')
self.res += imgImporter.run()
os.remove(nextImage)
else:
noMoreImages = True
return self.res
# Compute size of images -------------------------------------------------------
jpgTypes = ('jpg', 'jpeg')
pxToCm = 44.173513561
def getSize(filePath, fileType):
'''Gets the size of an image by reading first bytes.'''
x, y = (None, None)
f = file(filePath)
if fileType in jpgTypes:
# Dummy read to skip header ID
f.read(2)
while True:
# Extract the segment header.
(marker, code, length) = struct.unpack("!BBH", f.read(4))
# Verify that it's a valid segment.
if marker != 0xFF:
# No JPEG marker
break
elif code >= 0xC0 and code <= 0xC3:
# Segments that contain size info
(y, x) = struct.unpack("!xHH", f.read(5))
break
else:
# Dummy read to skip over data
f.read(length-2)
elif fileType == 'png':
# Dummy read to skip header data
f.read(12)
if f.read(4) == "IHDR":
x, y = struct.unpack("!LL", f.read(8))
elif fileType == 'gif':
imgType = f.read(6)
buf = f.read(5)
if len(buf) == 5:
# else: invalid/corrupted GIF (bad header)
x, y, u = struct.unpack("<HHB", buf)
return float(x)/pxToCm, float(y)/pxToCm
class ImageImporter(DocImporter):
'''This class allows to import into the ODT result an image stored
externally.'''
anchorTypes = ('page', 'paragraph', 'char', 'as-char')
WRONG_ANCHOR = 'Wrong anchor. Valid values for anchors are: %s.'
def getImportFolder(self):
return '%s/unzip/Pictures' % self.tempFolder
def moveFile(self, at):
'''Image to insert is at p_at. We must move it into the ODT result.'''
fileName = os.path.basename(at)
folderName = self.getImportFolder()
if not os.path.exists(folderName):
os.mkdir(folderName)
res = self.getImportPath(fileName)
shutil.copy(at, res)
return res
def setAnchor(self, anchor):
if anchor not in self.anchorTypes:
raise PodError(self.WRONG_ANCHOR % str(self.anchorTypes))
self.anchor = anchor
def run(self):
# Some shorcuts for the used xml namespaces
d = self.drawNs
t = self.textNs
x = self.linkNs
s = self.svgNs
imageName = 'Image%f' % time.time()
# Compute path to image
i = self.importPath.rfind('/Pictures/')
imagePath = self.importPath[i+1:]
# Compute image size
width, height = getSize(self.importPath, self.format)
if width != None:
size = ' %s:width="%fcm" %s:height="%fcm"' % (s, width, s, height)
else:
size = ''
self.res += '<%s:p><%s:frame %s:name="%s" %s:z-index="0" ' \
'%s:anchor-type="%s"%s><%s:image %s:type="simple" ' \
'%s:show="embed" %s:href="%s" %s:actuate="onLoad"/>' \
'</%s:frame></%s:p>' % \
(t, d, d, imageName, d, t, self.anchor, size, d, x, x, x,
imagePath, x, d, t)
return self.res
# ------------------------------------------------------------------------------

87
pod/elements.py Executable file
View file

@ -0,0 +1,87 @@
# ------------------------------------------------------------------------------
# Appy is a framework for building applications in the Python language.
# Copyright (C) 2007 Gaetan Delannay
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,USA.
# ------------------------------------------------------------------------------
from appy.shared.xml_parser import XmlElement
from appy.pod.odf_parser import OdfEnvironment as ns
from appy.pod import PodError
# ------------------------------------------------------------------------------
class PodElement:
OD_TO_POD = {'p': 'Text', 'h': 'Title', 'section': 'Section',
'table': 'Table', 'table-row': 'Row', 'table-cell': 'Cell',
None: 'Expression'}
POD_ELEMS = ('text', 'title', 'section', 'table', 'row', 'cell')
MINUS_ELEMS = ('section', 'table') # Elements for which the '-' operator can
# be applied
def create(elem):
'''Used to create any POD elem that has a equivalent OD element. Not
for creating expressions, for example.'''
return eval(PodElement.OD_TO_POD[elem])()
create = staticmethod(create)
class Text(PodElement):
OD = XmlElement('p', nsUri=ns.NS_TEXT)
subTags = [] # When generating an error we may need to surround the error
# with a given tag and subtags
class Title(PodElement):
OD = XmlElement('h', nsUri=ns.NS_TEXT)
subTags = []
class Section(PodElement):
OD = XmlElement('section', nsUri=ns.NS_TEXT)
subTags = [Text.OD]
DEEPEST_TO_REMOVE = OD # When we must remove the Section element from a
# buffer, the deepest element to remove is the Section element itself
class Cell(PodElement):
OD = XmlElement('table-cell', nsUri=ns.NS_TABLE)
subTags = [Text.OD]
def __init__(self):
self.tableInfo = None # ~OdTable~
class Row(PodElement):
OD = XmlElement('table-row', nsUri=ns.NS_TABLE)
subTags = [Cell.OD, Text.OD]
class Table(PodElement):
OD = XmlElement('table', nsUri=ns.NS_TABLE)
subTags = [Row.OD, Cell.OD, Text.OD]
DEEPEST_TO_REMOVE = Cell.OD # When we must remove the Table element from a
# buffer, the deepest element to remove is the Cell (it can only be done for
# one-row, one-cell tables
def __init__(self):
self.tableInfo = None # ~OdTable~
class Expression(PodElement):
OD = None
def __init__(self, pyExpr):
self.expr = pyExpr
def evaluate(self, context):
res = eval(self.expr, context)
if res == None:
res = u''
elif isinstance(res, str):
res = unicode(res.decode('utf-8'))
elif isinstance(res, unicode):
pass
else:
res = unicode(res)
return res
# ------------------------------------------------------------------------------

51
pod/odf_parser.py Executable file
View file

@ -0,0 +1,51 @@
# ------------------------------------------------------------------------------
# Appy is a framework for building applications in the Python language.
# Copyright (C) 2007 Gaetan Delannay
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,USA.
# ------------------------------------------------------------------------------
from appy.shared.xml_parser import XmlEnvironment, XmlParser
class OdfEnvironment(XmlEnvironment):
'''This environment is specific for parsing ODF files.'''
# URIs of namespaces found in ODF files
NS_OFFICE = 'urn:oasis:names:tc:opendocument:xmlns:office:1.0'
NS_STYLE = 'urn:oasis:names:tc:opendocument:xmlns:style:1.0'
NS_TEXT = 'urn:oasis:names:tc:opendocument:xmlns:text:1.0'
NS_TABLE = 'urn:oasis:names:tc:opendocument:xmlns:table:1.0'
NS_DRAW = 'urn:oasis:names:tc:opendocument:xmlns:drawing:1.0'
NS_FO = 'urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0'
NS_XLINK = 'http://www.w3.org/1999/xlink'
NS_DC = 'http://purl.org/dc/elements/1.1/'
NS_META = 'urn:oasis:names:tc:opendocument:xmlns:meta:1.0'
NS_NUMBER = 'urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0'
NS_SVG = 'urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0'
NS_CHART = 'urn:oasis:names:tc:opendocument:xmlns:chart:1.0'
NS_DR3D = 'urn:oasis:names:tc:opendocument:xmlns:dr3d:1.0'
NS_MATH = 'http://www.w3.org/1998/Math/MathML'
NS_FORM = 'urn:oasis:names:tc:opendocument:xmlns:form:1.0'
NS_SCRIPT = 'urn:oasis:names:tc:opendocument:xmlns:script:1.0'
NS_OOO = 'http://openoffice.org/2004/office'
NS_OOOW = 'http://openoffice.org/2004/writer'
NS_OOOC = 'http://openoffice.org/2004/calc'
NS_DOM = 'http://www.w3.org/2001/xml-events'
NS_XFORMS = 'http://www.w3.org/2002/xforms'
NS_XSD = 'http://www.w3.org/2001/XMLSchema'
NS_XSI = 'http://www.w3.org/2001/XMLSchema-instance'
class OdfParser(XmlParser):
'''XML parser that is specific for parsing ODF files.'''
# ------------------------------------------------------------------------------

303
pod/pod_parser.py Executable file
View file

@ -0,0 +1,303 @@
# ------------------------------------------------------------------------------
# Appy is a framework for building applications in the Python language.
# Copyright (C) 2007 Gaetan Delannay
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,USA.
# ------------------------------------------------------------------------------
import re
from appy.shared.xml_parser import XmlElement
from appy.pod.buffers import FileBuffer, MemoryBuffer
from appy.pod.odf_parser import OdfEnvironment, OdfParser
from appy.pod.elements import *
# ------------------------------------------------------------------------------
class OdTable:
'''Informations about the currently parsed Open Document (Od)table.'''
def __init__(self):
self.nbOfColumns = 0
self.nbOfRows = 0
self.curColIndex = None
self.curRowAttrs = None
def isOneCell(self):
return (self.nbOfColumns == 1) and (self.nbOfRows == 1)
class OdInsert:
'''While parsing an odt/pod file, we may need to insert a specific odt chunk
at a given place in the odt file (ie: add the pod-specific fonts and
styles). OdInsert instances define such 'inserts' (what to insert and
when).'''
def __init__(self, odtChunk, elem, nsUris={}):
self.odtChunk = odtChunk.decode('utf-8') # The odt chunk to insert
self.elem = elem # The p_odtChunk will be inserted just after the p_elem
# start, which must be an XmlElement instance. If more than one p_elem
# is present in the odt file, the p_odtChunk will be inserted only at
# the first p_elem occurrence.
self.nsUris = nsUris # The URI replacements that need to be done in
# p_odtChunk. It is a dict whose keys are names used in p_odtChunk (in
# the form @name@) to refer to XML namespaces, and values are URIs of
# those namespaces.
def resolve(self, namespaces):
'''Replaces all unresolved namespaces in p_odtChunk, thanks to the dict
of p_namespaces.'''
for nsName, nsUri in self.nsUris.iteritems():
self.odtChunk = re.sub('@%s@' % nsName, namespaces[nsUri],
self.odtChunk)
return self.odtChunk
class PodEnvironment(OdfEnvironment):
'''Contains all elements representing the current parser state during
parsing.'''
# Elements we must ignore (they will not be included in the result
ignorableElements = None # Will be set after namespace propagation
# Elements that may be impacted by POD statements
impactableElements = None # Idem
# Possibles modes
# ADD_IN_BUFFER: when encountering an impactable element, we must
# continue to dump it in the current buffer
ADD_IN_BUFFER = 0
# ADD_IN_SUBBUFFER: when encountering an impactable element, we must
# create a new subbuffer and dump it in it.
ADD_IN_SUBBUFFER = 1
# Possible states
IGNORING = 0 # We are ignoring what we are currently reading
READING_CONTENT = 1 # We are reading "normal" content
READING_STATEMENT = 2
# We are reading a POD statement (for, if...), which is located within a
# office:annotation element
READING_EXPRESSION = 3
# We are reading a POD expression, which is located between
# a text:change-start and a text:change-end elements
def __init__(self, context, inserts=[]):
OdfEnvironment.__init__(self)
# Buffer where we must dump the content we are currently reading
self.currentBuffer = None
# XML element content we are currently reading
self.currentContent = ''
# Current statement (a list of lines) that we are currently reading
self.currentStatement = []
# Current mode
self.mode = self.ADD_IN_SUBBUFFER
# Current state
self.state = self.READING_CONTENT
# Stack of currently visited tables
self.tableStack = []
self.tableIndex = -1
# Evaluation context
self.context = context
# For the currently read expression, is there style-related information
# associated with it?
self.exprHasStyle = False
self.gotNamespaces = False # Namespace definitions were not already
# encountered
# Store inserts
self.inserts = inserts
# Currently walked "if" actions
self.ifActions = []
# Currently walked named "if" actions
self.namedIfActions = {} #~{s_statementName: IfAction}~
def getTable(self):
'''Gets the currently parsed table.'''
res = None
if self.tableIndex != -1:
res = self.tableStack[self.tableIndex]
return res
def transformInserts(self):
'''Now the namespaces were parsed; I can put p_inserts in the form of a
dict for easier and more performant access while parsing.'''
res = {}
for insert in self.inserts:
elemName = insert.elem.getFullName(self.namespaces)
if not res.has_key(elemName):
res[elemName] = insert
return res
def manageInserts(self):
'''We just dumped the start of an elem. Here we will insert any odt
chunk if needed.'''
if self.inserts.has_key(self.currentElem.elem):
insert = self.inserts[self.currentElem.elem]
self.currentBuffer.write(insert.resolve(self.namespaces))
# The insert is destroyed after single use
del self.inserts[self.currentElem.elem]
def onStartElement(self):
ns = self.namespaces
if not self.gotNamespaces:
# We suppose that all the interesting (from the POD point of view)
# XML namespace definitions are defined at the root XML element.
# Here we propagate them in XML element definitions that we use
# throughout POD.
self.gotNamespaces = True
self.propagateNamespaces()
elem = self.currentElem.elem
tableNs = self.ns(self.NS_TABLE)
if elem == Table.OD.elem:
self.tableStack.append(OdTable())
self.tableIndex += 1
elif elem == Row.OD.elem:
self.getTable().nbOfRows += 1
self.getTable().curColIndex = -1
self.getTable().curRowAttrs = self.currentElem.attrs
elif elem == Cell.OD.elem:
self.getTable().curColIndex += 1
elif elem == ('%s:table-column' % tableNs):
attrs = self.currentElem.attrs
if attrs.has_key('%s:number-columns-repeated' % tableNs):
self.getTable().nbOfColumns += int(
attrs['%s:number-columns-repeated' % tableNs])
else:
self.getTable().nbOfColumns += 1
return ns
def onEndElement(self):
ns = self.namespaces
if self.currentElem.elem == Table.OD.elem:
self.tableStack.pop()
self.tableIndex -= 1
return ns
def addSubBuffer(self):
subBuffer = self.currentBuffer.addSubBuffer()
self.currentBuffer = subBuffer
self.mode = self.ADD_IN_BUFFER
def propagateNamespaces(self):
'''Propagates the namespaces in all XML element definitions that are
used throughout POD.'''
ns = self.namespaces
for elemName in PodElement.POD_ELEMS:
xmlElemDef = eval(elemName[0].upper() + elemName[1:]).OD
elemFullName = xmlElemDef.getFullName(ns)
xmlElemDef.__init__(elemFullName)
self.ignorableElements = ('%s:tracked-changes' % ns[self.NS_TEXT],
'%s:change' % ns[self.NS_TEXT])
self.impactableElements = (
Text.OD.elem, Title.OD.elem, Table.OD.elem, Row.OD.elem,
Cell.OD.elem, Section.OD.elem)
self.inserts = self.transformInserts()
# ------------------------------------------------------------------------------
class PodParser(OdfParser):
def __init__(self, env, caller):
OdfParser.__init__(self, env, caller)
def endDocument(self):
self.env.currentBuffer.content.close()
def startElement(self, elem, attrs):
e = OdfParser.startElement(self, elem, attrs)
ns = e.onStartElement()
officeNs = ns[e.NS_OFFICE]
textNs = ns[e.NS_TEXT]
if elem in e.ignorableElements:
e.state = e.IGNORING
elif elem == ('%s:annotation' % officeNs):
e.state = e.READING_STATEMENT
elif elem == ('%s:change-start' % textNs):
e.state = e.READING_EXPRESSION
e.exprHasStyle = False
else:
if e.state == e.IGNORING:
pass
elif e.state == e.READING_CONTENT:
if elem in e.impactableElements:
if e.mode == e.ADD_IN_SUBBUFFER:
e.addSubBuffer()
e.currentBuffer.addElement(e.currentElem.name)
e.currentBuffer.dumpStartElement(elem, attrs)
elif e.state == e.READING_STATEMENT:
pass
elif e.state == e.READING_EXPRESSION:
if (elem == ('%s:span' % textNs)) and \
not e.currentContent.strip():
e.currentBuffer.dumpStartElement(elem, attrs)
e.exprHasStyle = True
e.manageInserts()
def endElement(self, elem):
e = OdfParser.endElement(self, elem)
ns = e.onEndElement()
officeNs = ns[e.NS_OFFICE]
textNs = ns[e.NS_TEXT]
if elem in e.ignorableElements:
e.state = e.READING_CONTENT
elif elem == ('%s:annotation' % officeNs):
# Manage statement
oldCb = e.currentBuffer
actionElemIndex = oldCb.createAction(e.currentStatement)
e.currentStatement = []
if actionElemIndex != -1:
e.currentBuffer = oldCb.\
transferActionIndependentContent(actionElemIndex)
if e.currentBuffer == oldCb:
e.mode = e.ADD_IN_SUBBUFFER
else:
e.mode = e.ADD_IN_BUFFER
e.state = e.READING_CONTENT
else:
if e.state == e.IGNORING:
pass
elif e.state == e.READING_CONTENT:
e.currentBuffer.dumpEndElement(elem)
if elem in e.impactableElements:
if isinstance(e.currentBuffer, MemoryBuffer):
isMainElement = e.currentBuffer.isMainElement(elem)
# Unreference the element among the 'elements' attribute
e.currentBuffer.unreferenceElement(elem)
if isMainElement:
parent = e.currentBuffer.parent
if not e.currentBuffer.action:
# Delete this buffer and transfer content to parent
e.currentBuffer.transferAllContent()
parent.removeLastSubBuffer()
e.currentBuffer = parent
else:
if isinstance(parent, FileBuffer):
# Execute buffer action and delete the buffer
e.currentBuffer.action.execute()
parent.removeLastSubBuffer()
e.currentBuffer = parent
e.mode = e.ADD_IN_SUBBUFFER
elif e.state == e.READING_STATEMENT:
if e.currentElem.elem == Text.OD.elem:
statementLine = e.currentContent.strip()
if statementLine:
e.currentStatement.append(statementLine)
e.currentContent = ''
elif e.state == e.READING_EXPRESSION:
if elem == ('%s:change-end' % textNs):
expression = e.currentContent.strip()
e.currentContent = ''
# Manage expression
e.currentBuffer.addExpression(expression)
if e.exprHasStyle:
e.currentBuffer.dumpEndElement('%s:span' % textNs)
e.state = e.READING_CONTENT
def characters(self, content):
e = OdfParser.characters(self, content)
if e.state == e.IGNORING:
pass
elif e.state == e.READING_CONTENT:
e.currentBuffer.dumpContent(content)
elif e.state == e.READING_STATEMENT:
if e.currentElem.elem.startswith(e.namespaces[e.NS_TEXT]):
e.currentContent += content
elif e.state == e.READING_EXPRESSION:
e.currentContent += content
# ------------------------------------------------------------------------------

447
pod/renderer.py Executable file
View file

@ -0,0 +1,447 @@
# ------------------------------------------------------------------------------
# Appy is a framework for building applications in the Python language.
# Copyright (C) 2007 Gaetan Delannay
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,USA.
# ------------------------------------------------------------------------------
import zipfile, shutil, xml.sax, os, os.path, re
from UserDict import UserDict
import appy.pod
from appy.pod import PodError
from appy.shared.xml_parser import XmlElement
from appy.pod.pod_parser import PodParser, PodEnvironment, OdInsert
from appy.pod.converter import ODT_FILE_TYPES
from appy.pod.buffers import FileBuffer
from appy.pod.xhtml2odt import Xhtml2OdtConverter
from appy.pod.doc_importers import OdtImporter, ImageImporter, PdfImporter
from appy.pod.styles_manager import StylesManager
from appy.shared.utils import FolderDeleter
# ------------------------------------------------------------------------------
BAD_CONTEXT = 'Context must be either a dict, a UserDict or an instance.'
RESULT_FILE_EXISTS = 'Result file "%s" exists.'
CANT_WRITE_RESULT = 'I cannot write result file "%s". %s'
TEMP_FOLDER_EXISTS = 'I need to use a temp folder "%s" but this folder ' \
'already exists.'
CANT_WRITE_TEMP_FOLDER = 'I cannot create temp folder "%s". %s'
NO_PY_PATH = 'Extension of result file is "%s". In order to perform ' \
'conversion from ODT to this format we need to call OpenOffice. ' \
'But the Python interpreter which runs the current script does ' \
'not know UNO, the library that allows to connect to ' \
'OpenOffice in server mode. If you can\'t install UNO in this ' \
'Python interpreter, you can specify, in parameter ' \
'"pythonWithUnoPath", the path to a UNO-enabled Python ' \
'interpreter. One such interpreter may be found in ' \
'<open_office_path>/program.'
PY_PATH_NOT_FILE = '"%s" is not a file. You must here specify the absolute ' \
'path of a Python interpreter (.../python, .../python.sh, ' \
'.../python.exe, .../python.bat...).'
BLANKS_IN_PATH = 'Blanks were found in path "%s". Please use the DOS-names ' \
'(ie, "progra~1" instead of "Program files" or "docume~1" ' \
'instead of "Documents and settings".'
BAD_RESULT_TYPE = 'Result "%s" has a wrong extension. Allowed extensions ' \
'are: "%s".'
CONVERT_ERROR = 'An error occurred during the conversion. %s'
BAD_OO_PORT = 'Bad OpenOffice port "%s". Make sure it is an integer.'
XHTML_ERROR = 'An error occurred while rendering XHTML content.'
WARNING_INCOMPLETE_ODT = 'Warning: your ODT file may not be complete (ie ' \
'imported documents may not be present). This is ' \
'because we could not connect to OpenOffice in ' \
'server mode: %s'
DOC_NOT_SPECIFIED = 'Please specify a document to import, either with a ' \
'stream (parameter "content") or with a path (parameter ' \
'"at")'
DOC_FORMAT_ERROR = 'POD was unable to deduce the document format. Please ' \
'specify it through parameter named "format" (=odt, gif, ' \
'png, ...).'
DOC_WRONG_FORMAT = 'Format "%s" is not supported.'
WARNING_FINALIZE_ERROR = 'Warning: error while calling finalize function. %s'
# Default automatic text styles added by pod in content.xml
f = open('%s/styles.in.content.xml' % os.path.dirname(appy.pod.__file__))
CONTENT_POD_STYLES = f.read()
f.close()
# Default font added by pod in content.xml
CONTENT_POD_FONTS = '<@style@:font-face style:name="PodStarSymbol" ' \
'@svg@:font-family="StarSymbol"/>'
# Default text styles added by pod in styles.xml
f = file('%s/styles.in.styles.xml' % os.path.dirname(appy.pod.__file__))
STYLES_POD_STYLES = f.read()
f.close()
# Default font added by pod
STYLES_POD_FONTS = '<@style@:font-face @style@:name="PodStarSymbol" ' \
'@svg@:font-family="StarSymbol"/>'
# ------------------------------------------------------------------------------
class Renderer:
def __init__(self, template, context, result, pythonWithUnoPath=None,
ooPort=2002, stylesMapping={}, forceOoCall=False,
finalizeFunction=None):
'''This Python Open Document Renderer (PodRenderer) loads a document
template (p_template) which is an ODT file with some elements
written in Python. Based on this template and some Python objects
defined in p_context, the renderer generates an ODT file
(p_result) that instantiates the p_template and fills it with objects
from the p_context.
- If p_result does not end with .odt, the Renderer
will call OpenOffice to perform a conversion. If p_forceOoCall is
True, even if p_result ends with .odt, OpenOffice will be called, not
for performing a conversion, but for updating some elements like
indexes (table of contents, etc) and sections containing links to
external files (which is the case, for example, if you use the
default function "document").
- If the Python interpreter which runs the current script is not
UNO-enabled, this script will run, in another process, a UNO-enabled
Python interpreter (whose path is p_pythonWithUnoPath) which will
call OpenOffice. In both cases, we will try to connect to OpenOffice
in server mode on port p_ooPort.
- If you plan to make "XHTML to OpenDocument" conversions, you may
specify a styles mapping in p_stylesMapping.
- If you specify a function in p_finalizeFunction, this function will
be called by the renderer before re-zipping the ODT result. This way,
you can still perform some actions on the content of the ODT file
before it is zipped and potentially converted. This function must
accept one arg: the absolute path to the temporary folder containing
the un-zipped content of the ODT result.'''
self.template = template
self.templateZip = zipfile.ZipFile(template)
self.result = result
self.contentXml = None # Content (string) of content.xml
self.stylesXml = None # Content (string) of styles.xml
self.stylesManager = None # Manages the styles defined into the ODT
# template
self.tempFolder = None
self.curdir = os.getcwd()
self.env = None
self.pyPath = pythonWithUnoPath
self.ooPort = ooPort
self.forceOoCall = forceOoCall
self.finalizeFunction = finalizeFunction
self.prepareFolders()
# Unzip template
self.unzipFolder = os.path.join(self.tempFolder, 'unzip')
os.mkdir(self.unzipFolder)
for zippedFile in self.templateZip.namelist():
fileName = os.path.basename(zippedFile)
folderName = os.path.dirname(zippedFile)
# Create folder if needed
fullFolderName = self.unzipFolder
if folderName:
fullFolderName = os.path.join(fullFolderName, folderName)
if not os.path.exists(fullFolderName):
os.makedirs(fullFolderName)
# Unzip file
if fileName:
fullFileName = os.path.join(fullFolderName, fileName)
f = open(fullFileName, 'wb')
fileContent = self.templateZip.read(zippedFile)
if fileName == 'content.xml':
self.contentXml = fileContent
elif fileName == 'styles.xml':
self.stylesManager = StylesManager(fileContent)
self.stylesXml = fileContent
f.write(fileContent)
f.close()
self.templateZip.close()
# Create the content.xml parser
pe = PodEnvironment
contentInserts = (
OdInsert(CONTENT_POD_FONTS,
XmlElement('font-face-decls', nsUri=pe.NS_OFFICE),
nsUris={'style': pe.NS_STYLE, 'svg': pe.NS_SVG}),
OdInsert(CONTENT_POD_STYLES,
XmlElement('automatic-styles', nsUri=pe.NS_OFFICE),
nsUris={'style': pe.NS_STYLE, 'fo': pe.NS_FO,
'text': pe.NS_TEXT, 'table': pe.NS_TABLE}))
self.contentParser = self.createPodParser('content.xml', context,
contentInserts)
# Create the styles.xml parser
stylesInserts = (
OdInsert(STYLES_POD_FONTS,
XmlElement('font-face-decls', nsUri=pe.NS_OFFICE),
nsUris={'style': pe.NS_STYLE, 'svg': pe.NS_SVG}),
OdInsert(STYLES_POD_STYLES,
XmlElement('styles', nsUri=pe.NS_OFFICE),
nsUris={'style': pe.NS_STYLE, 'fo': pe.NS_FO}))
self.stylesParser = self.createPodParser('styles.xml', context,
stylesInserts)
# Stores the styles mapping
self.setStylesMapping(stylesMapping)
def createPodParser(self, odtFile, context, inserts):
'''Creates the parser with its environment for parsing the given
p_odtFile (content.xml or styles.xml). p_context is given by the pod
user, while p_inserts depends on the ODT file we must parse.'''
evalContext = {'xhtml': self.renderXhtml,
'test': self.evalIfExpression,
'document': self.importDocument} # Default context
if hasattr(context, '__dict__'):
evalContext.update(context.__dict__)
elif isinstance(context, dict) or isinstance(context, UserDict):
evalContext.update(context)
else:
raise PodError(BAD_CONTEXT)
env = PodEnvironment(evalContext, inserts)
fileBuffer = FileBuffer(env, os.path.join(self.tempFolder,odtFile))
env.currentBuffer = fileBuffer
return PodParser(env, self)
def renderXhtml(self, xhtmlString, encoding='utf-8', stylesMapping={}):
'''Method that can be used (under the name 'xhtml') into a pod template
for converting a chunk of XHTML content (p_xhtmlString) into a chunk
of ODT content.'''
stylesMapping = self.stylesManager.checkStylesMapping(stylesMapping)
ns = self.currentParser.env.namespaces
# xhtmlString is only a chunk of XHTML. So we must surround it a tag in
# order to get a XML-compliant file (we need a root tag)
xhtmlContent = '<podXhtml>%s</podXhtml>' % xhtmlString
return Xhtml2OdtConverter(xhtmlContent, encoding, self.stylesManager,
stylesMapping, ns).run()
def evalIfExpression(self, condition, ifTrue, ifFalse):
'''This method implements the method 'test' which is proposed in the
default pod context. It represents an 'if' expression (as opposed to
the 'if' statement): depending on p_condition, expression result is
p_ifTrue or p_ifFalse.'''
if condition:
return ifTrue
return ifFalse
imageFormats = ('png', 'jpeg', 'jpg', 'gif')
mimeTypes = {
'application/vnd.oasis.opendocument.text': 'odt',
'application/msword': 'doc', 'text/rtf': 'rtf',
'application/pdf' : 'pdf', 'image/png': 'png',
'image/jpeg': 'jpg', 'image/gif': 'gif'}
ooFormats = ('odt',)
def importDocument(self, content=None, at=None, format=None,
anchor='as-char'):
'''If p_at is not None, it represents a path or url allowing to find
the document. If p_at is None, the content of the document is
supposed to be in binary format in p_content. The document
p_format may be: odt or any format in imageFormats. p_anchor is only
relevant for images.'''
ns = self.currentParser.env.namespaces
importer = None
# Is there someting to import?
if not content and not at:
raise PodError(DOC_NOT_SPECIFIED)
# Guess document format
if not format:
# It should be deduced from p_at
if not at:
raise PodError(DOC_FORMAT_ERROR)
format = os.path.splitext(at)[1][1:]
else:
# If format is a mimeType, convert it to an extension
if self.mimeTypes.has_key(format):
format = self.mimeTypes[format]
isImage = False
if format in self.ooFormats:
importer = OdtImporter
self.forceOoCall = True
elif format in self.imageFormats:
importer = ImageImporter
isImage = True
elif format == 'pdf':
importer = PdfImporter
else:
raise PodError(DOC_WRONG_FORMAT % format)
imp = importer(content, at, format, self.tempFolder, ns)
if isImage:
imp.setAnchor(anchor)
return imp.run()
def prepareFolders(self):
# Check if I can write the result
if os.path.exists(self.result):
raise PodError(RESULT_FILE_EXISTS % self.result)
try:
f = open(self.result, 'w')
f.write('Hello')
f.close()
except OSError, oe:
raise PodError(CANT_WRITE_RESULT % (self.result, oe))
except IOError, ie:
raise PodError(CANT_WRITE_RESULT % (self.result, oe))
self.result = os.path.abspath(self.result)
os.remove(self.result)
# Check that temp folder does not exist
self.tempFolder = os.path.abspath(self.result) + '.temp'
if os.path.exists(self.tempFolder):
raise PodError(TEMP_FOLDER_EXISTS % self.tempFolder)
try:
os.mkdir(self.tempFolder)
except OSError, oe:
raise PodError(CANT_WRITE_TEMP_FOLDER % (self.result, oe))
# Public interface
def run(self):
'''Renders the result.'''
# Remember which parser is running
self.currentParser = self.contentParser
# Create the resulting content.xml
self.currentParser.parse(self.contentXml)
self.currentParser = self.stylesParser
# Create the resulting styles.xml
self.currentParser.parse(self.stylesXml)
# Re-zip the result
self.finalize()
def getStyles(self):
'''Returns a dict of the styles that are defined into the template.'''
return self.stylesManager.styles
def setStylesMapping(self, stylesMapping):
'''Establishes a correspondance between, on one hand, CSS styles or
XHTML tags that will be found inside XHTML content given to POD,
and, on the other hand, ODT styles found into the template.'''
try:
stylesMapping = self.stylesManager.checkStylesMapping(stylesMapping)
self.stylesManager.stylesMapping = stylesMapping
except PodError, po:
if os.path.exists(self.tempFolder):
FolderDeleter.delete(self.tempFolder)
raise po
def reportProblem(self, msg, resultType):
'''When trying to call OO in server mode for producing ODT
(=forceOoCall=True), if an error occurs we still have an ODT to
return to the user. So we produce a warning instead of raising an
error.'''
if (resultType == 'odt') and self.forceOoCall:
print WARNING_INCOMPLETE_ODT % msg
else:
raise msg
def callOpenOffice(self, resultOdtName, resultType):
'''Call Open Office in server mode to convert or update the ODT
result.'''
try:
if (not isinstance(self.ooPort, int)) and \
(not isinstance(self.ooPort, long)):
raise PodError(BAD_OO_PORT % str(self.ooPort))
try:
from appy.pod.converter import Converter, ConverterError
try:
Converter(resultOdtName, resultType,
self.ooPort).run()
except ConverterError, ce:
raise PodError(CONVERT_ERROR % str(ce))
except ImportError:
# I do not have UNO. So try to launch a UNO-enabled Python
# interpreter which should be in self.pyPath.
if not self.pyPath:
raise PodError(NO_PY_PATH % resultType)
if self.pyPath.find(' ') != -1:
raise PodError(BLANKS_IN_PATH % self.pyPath)
if not os.path.isfile(self.pyPath):
raise PodError(PY_PATH_NOT_FILE % self.pyPath)
if resultOdtName.find(' ') != -1:
qResultOdtName = '"%s"' % resultOdtName
else:
qResultOdtName = resultOdtName
convScript = '%s/converter.py' % \
os.path.dirname(appy.pod.__file__)
if convScript.find(' ') != -1:
convScript = '"%s"' % convScript
cmd = '%s %s %s %s -p%d' % \
(self.pyPath, convScript, qResultOdtName, resultType,
self.ooPort)
prgPipes = os.popen3(cmd)
convertOutput = prgPipes[2].read()
for pipe in prgPipes:
pipe.close()
if convertOutput:
# Remove warnings
errors = []
for error in convertOutput.split('\n'):
error = error.strip()
if not error:
continue
elif error.startswith('warning'):
pass
else:
errors.append(error)
if errors:
raise PodError(CONVERT_ERROR % '\n'.join(errors))
except PodError, pe:
# When trying to call OO in server mode for producing
# ODT (=forceOoCall=True), if an error occurs we still
# have an ODT to return to the user. So we produce a
# warning instead of raising an error.
if (resultType == 'odt') and self.forceOoCall:
print WARNING_INCOMPLETE_ODT % str(pe)
else:
raise pe
def finalize(self):
'''Re-zip the result and potentially call OpenOffice if target format is
not ODT or if forceOoCall is True.'''
for odtFile in ('content.xml', 'styles.xml'):
shutil.copy(os.path.join(self.tempFolder, odtFile),
os.path.join(self.unzipFolder, odtFile))
if self.finalizeFunction:
try:
self.finalizeFunction(self.unzipFolder)
except Exception, e:
print WARNING_FINALIZE_ERROR % str(e)
resultOdtName = os.path.join(self.tempFolder, 'result.odt')
resultOdt = zipfile.ZipFile(resultOdtName, 'w')
os.chdir(self.unzipFolder)
for dir, dirnames, filenames in os.walk('.'):
for f in filenames:
resultOdt.write(os.path.join(dir, f)[2:])
# [2:] is there to avoid havin './' in the path in the zip file.
resultOdt.close()
resultType = os.path.splitext(self.result)[1]
try:
if (resultType == '.odt') and not self.forceOoCall:
# Simply move the ODT result to the result
os.rename(resultOdtName, self.result)
else:
if resultType.startswith('.'): resultType = resultType[1:]
if not resultType in ODT_FILE_TYPES.keys():
raise PodError(BAD_RESULT_TYPE % (
self.result, ODT_FILE_TYPES.keys()))
# Call OpenOffice to perform the conversion or document update
self.callOpenOffice(resultOdtName, resultType)
# I have the result. Move it to the correct name
resPrefix = os.path.splitext(resultOdtName)[0] + '.'
if resultType == 'odt':
# converter.py has (normally!) created a second file
# suffixed .res.odt
resultName = resPrefix + 'res.odt'
if not os.path.exists(resultName):
resultName = resultOdtName
# In this case OO in server mode could not be called to
# update indexes, sections, etc.
else:
resultName = resPrefix + resultType
os.rename(resultName, self.result)
finally:
os.chdir(self.curdir)
FolderDeleter.delete(self.tempFolder)
# ------------------------------------------------------------------------------

107
pod/styles.in.content.xml Executable file
View file

@ -0,0 +1,107 @@
<@style@:style @style@:name="podTable" @style@:family="table">
<@style@:table-properties @table@:align="margins"/>
</@style@:style>
<@style@:style @style@:name="podCell" @style@:family="table-cell">
<@style@:table-cell-properties @fo@:padding="0.097cm" @fo@:border="0.002cm solid #000000"/>
</@style@:style>
<@style@:style @style@:name="podHeaderCell" @style@:family="table-cell">
<@style@:table-cell-properties @fo@:background-color="#e6e6e6" @fo@:padding="0.097cm" @fo@:border="0.002cm solid #000000"/>
</@style@:style>
<@style@:style @style@:name="podItalic" @style@:family="text">
<@style@:text-properties @fo@:font-style="italic" @style@:font-style-asian="italic"
@style@:font-style-complex="italic"/>
</@style@:style>
<@style@:style @style@:name="podBold" @style@:family="text">
<@style@:text-properties @fo@:font-weight="bold" @style@:font-weight-asian="bold"
@style@:font-weight-complex="bold"/>
</@style@:style>
<@style@:style @style@:name="podSup" @style@:family="text">
<@style@:text-properties @style@:text-position="super 58%"/>
</@style@:style>
<@style@:style @style@:name="podSub" @style@:family="text">
<@style@:text-properties @style@:text-position="sub 58%"/>
</@style@:style>
<@style@:style @style@:name="podBulletItem" @style@:family="paragraph" @style@:list-style-name="podBulletedList"/>
<@style@:style @style@:name="podNumberItem" @style@:family="paragraph" @style@:list-style-name="podNumberedList"/>
<@style@:style @style@:name="podBulletItemKeepWithNext" @style@:family="paragraph"
@style@:list-style-name="podBulletedList">
<@style@:paragraph-properties @fo@:keep-with-next="always"/>
</@style@:style>
<@style@:style @style@:name="podNumberItemKeepWithNext" @style@:family="paragraph"
@style@:list-style-name="podNumberedList">
<@style@:paragraph-properties @fo@:keep-with-next="always"/>
</@style@:style>
<@text@:list-style @style@:name="podBulletedList">
<@text@:list-level-style-bullet @text@:level="1" @text@:style-name="podBulletStyle" @style@:num-suffix="." @text@:bullet-char="●">
<@style@:list-level-properties @text@:space-before="0.25in" @text@:min-label-width="0.25in"/>
<@style@:text-properties @style@:font-name="PodStarSymbol"/>
</@text@:list-level-style-bullet>
<@text@:list-level-style-bullet @text@:level="2" @text@:style-name="podBulletStyle" @style@:num-suffix="." @text@:bullet-char="○">
<@style@:list-level-properties @text@:space-before="0.5in" @text@:min-label-width="0.25in"/>
<@style@:text-properties @style@:font-name="PodStarSymbol"/>
</@text@:list-level-style-bullet>
<@text@:list-level-style-bullet @text@:level="3" @text@:style-name="podBulletStyle" @style@:num-suffix="." @text@:bullet-char="■">
<@style@:list-level-properties @text@:space-before="0.75in" @text@:min-label-width="0.25in"/>
<@style@:text-properties @style@:font-name="PodStarSymbol"/>
</@text@:list-level-style-bullet>
<@text@:list-level-style-bullet @text@:level="4" @text@:style-name="podBulletStyle" @style@:num-suffix="." @text@:bullet-char="●">
<@style@:list-level-properties @text@:space-before="1in" @text@:min-label-width="0.25in"/>
<@style@:text-properties @style@:font-name="PodStarSymbol"/>
</@text@:list-level-style-bullet>
<@text@:list-level-style-bullet @text@:level="5" @text@:style-name="podBulletStyle" @style@:num-suffix="." @text@:bullet-char="○">
<@style@:list-level-properties @text@:space-before="1.25in" @text@:min-label-width="0.25in"/>
<@style@:text-properties @style@:font-name="PodStarSymbol"/>
</@text@:list-level-style-bullet>
<@text@:list-level-style-bullet @text@:level="6" @text@:style-name="podBulletStyle" @style@:num-suffix="." @text@:bullet-char="■">
<@style@:list-level-properties @text@:space-before="1.5in" @text@:min-label-width="0.25in"/>
<@style@:text-properties @style@:font-name="PodStarSymbol"/>
</@text@:list-level-style-bullet>
<@text@:list-level-style-bullet @text@:level="7" @text@:style-name="podBulletStyle" @style@:num-suffix="." @text@:bullet-char="●">
<@style@:list-level-properties @text@:space-before="1.75in" @text@:min-label-width="0.25in"/>
<@style@:text-properties @style@:font-name="PodStarSymbol"/>
</@text@:list-level-style-bullet>
<@text@:list-level-style-bullet @text@:level="8" @text@:style-name="podBulletStyle" @style@:num-suffix="." @text@:bullet-char="○">
<@style@:list-level-properties @text@:space-before="2in" @text@:min-label-width="0.25in"/>
<@style@:text-properties @style@:font-name="PodStarSymbol"/>
</@text@:list-level-style-bullet>
<@text@:list-level-style-bullet @text@:level="9" @text@:style-name="podBulletStyle" @style@:num-suffix="." @text@:bullet-char="■">
<@style@:list-level-properties @text@:space-before="2.25in" @text@:min-label-width="0.25in"/>
<@style@:text-properties @style@:font-name="PodStarSymbol"/>
</@text@:list-level-style-bullet>
<@text@:list-level-style-bullet @text@:level="10" @text@:style-name="podBulletStyle" @style@:num-suffix="." @text@:bullet-char="●">
<@style@:list-level-properties @text@:space-before="2.5in" @text@:min-label-width="0.25in"/>
<@style@:text-properties @style@:font-name="PodStarSymbol"/>
</@text@:list-level-style-bullet>
</@text@:list-style>
<@text@:list-style @style@:name="podNumberedList">
<@text@:list-level-style-number @text@:level="1" @text@:style-name="podNumberStyle" @style@:num-suffix="." @style@:num-format="1">
<@style@:list-level-properties @text@:space-before="0.25in" @text@:min-label-width="0.25in"/>
</@text@:list-level-style-number>
<@text@:list-level-style-number @text@:level="2" @text@:style-name="podNumberStyle" @style@:num-suffix="." @style@:num-format="1">
<@style@:list-level-properties @text@:space-before="0.5in" @text@:min-label-width="0.25in"/>
</@text@:list-level-style-number>
<@text@:list-level-style-number @text@:level="3" @text@:style-name="podNumberStyle" @style@:num-suffix="." @style@:num-format="1">
<@style@:list-level-properties @text@:space-before="0.75in" @text@:min-label-width="0.25in"/>
</@text@:list-level-style-number>
<@text@:list-level-style-number @text@:level="4" @text@:style-name="podNumberStyle" @style@:num-suffix="." @style@:num-format="1">
<@style@:list-level-properties @text@:space-before="1in" @text@:min-label-width="0.25in"/>
</@text@:list-level-style-number>
<@text@:list-level-style-number @text@:level="5" @text@:style-name="podNumberStyle" @style@:num-suffix="." @style@:num-format="1">
<@style@:list-level-properties @text@:space-before="1.25in" @text@:min-label-width="0.25in"/>
</@text@:list-level-style-number>
<@text@:list-level-style-number @text@:level="6" @text@:style-name="podNumberStyle" @style@:num-suffix="." @style@:num-format="1">
<@style@:list-level-properties @text@:space-before="1.5in" @text@:min-label-width="0.25in"/>
</@text@:list-level-style-number>
<@text@:list-level-style-number @text@:level="7" @text@:style-name="podNumberStyle" @style@:num-suffix="." @style@:num-format="1">
<@style@:list-level-properties @text@:space-before="1.75in" @text@:min-label-width="0.25in"/>
</@text@:list-level-style-number>
<@text@:list-level-style-number @text@:level="8" @text@:style-name="podNumberStyle" @style@:num-suffix="." @style@:num-format="1">
<@style@:list-level-properties @text@:space-before="2in" @text@:min-label-width="0.25in"/>
</@text@:list-level-style-number>
<@text@:list-level-style-number @text@:level="9" @text@:style-name="podNumberStyle" @style@:num-suffix="." @style@:num-format="1">
<@style@:list-level-properties @text@:space-before="2.25in" @text@:min-label-width="0.25in"/>
</@text@:list-level-style-number>
<@text@:list-level-style-number @text@:level="10" @text@:style-name="podNumberStyle" @style@:num-suffix="." @style@:num-format="1">
<@style@:list-level-properties @text@:space-before="2.5in" @text@:min-label-width="0.25in"/>
</@text@:list-level-style-number>
</@text@:list-style>

6
pod/styles.in.styles.xml Executable file
View file

@ -0,0 +1,6 @@
<@style@:style @style@:name="podNumberStyle" @style@:display-name="POD Numbering Symbols" @style@:family="text"/>
<@style@:style @style@:name="podBulletStyle" @style@:display-name="POD Bullet Symbols" @style@:family="text">
<@style@:text-properties @style@:font-name="PodStarSymbol" @fo@:font-size="9pt"
@style@:font-name-asian="PodStarSymbol" @style@:font-size-asian="9pt"
@style@:font-name-complex="PodStarSymbol" @style@:font-size-complex="9pt"/>
</@style@:style>

238
pod/styles_manager.py Executable file
View file

@ -0,0 +1,238 @@
# ------------------------------------------------------------------------------
# Appy is a framework for building applications in the Python language.
# Copyright (C) 2007 Gaetan Delannay
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,USA.
# ------------------------------------------------------------------------------
import re, os.path
from UserDict import UserDict
import appy.pod
from appy.pod import *
from appy.pod.odf_parser import OdfEnvironment, OdfParser
# Possible states for the parser
READING = 0 # Default state
PARSING_STYLE = 1 # I am parsing styles definitions
# Error-related constants ------------------------------------------------------
MAPPING_NOT_DICT = 'The styles mapping must be a dictionary or a UserDict ' \
'instance.'
MAPPING_ELEM_NOT_STRING = "The styles mapping dictionary's keys and values " \
"must be strings."
MAPPING_OUTLINE_DELTA_NOT_INT = 'When specifying "h*" as key in the styles ' \
'mapping, you must specify an integer as ' \
'value. This integer, which may be positive ' \
'or negative, represents a delta that will ' \
'be added to the html heading\'s outline ' \
'level for finding an ODT style with the ' \
'same outline level.'
MAPPING_ELEM_EMPTY = 'In your styles mapping, you inserted an empty key ' \
'and/or value.'
UNSTYLABLE_TAG = 'You can\'t associate a style to element "%s". Unstylable ' \
'elements are: %s'
STYLE_NOT_FOUND = 'OpenDocument style "%s" was not found in your template. ' \
'Note that the styles names ("Heading 1", "Standard"...) ' \
'that appear when opening your template with OpenOffice, ' \
'for example, are a super-set of the styles that are really '\
'recorded into your document. Indeed, only styles that are ' \
'in use within your template are actually recorded into ' \
'the document. You may consult the list of available ' \
'styles programmatically by calling your pod renderer\'s ' \
'"getStyles" method.'
HTML_PARA_ODT_TEXT = 'For XHTML element "%s", you must associate a ' \
'paragraph-wide OpenDocument style. "%s" is a "text" ' \
'style (that applies to only a chunk of text within a ' \
'paragraph).'
HTML_TEXT_ODT_PARA = 'For XHTML element "%s", you must associate an ' \
'OpenDocument "text" style (that applies to only a chunk '\
'of text within a paragraph). "%s" is a paragraph-wide ' \
'style.'
# ------------------------------------------------------------------------------
class Style:
'''Represents a paragraph style as found in styles.xml in a ODT file.'''
numberRex = re.compile('(\d+)(.*)')
def __init__(self, name, family):
self.name = name
self.family = family # May be 'paragraph', etc.
self.displayName = name
self.styleClass = None # May be 'text', 'list', etc.
self.fontSize = None
self.fontSizeUnit = None # May be pt, %, ...
self.outlineLevel = None # Were the styles lies within styles and
# substyles hierarchy
def setFontSize(self, fontSize):
rexRes = self.numberRex.search(fontSize)
self.fontSize = int(rexRes.group(1))
self.fontSizeUnit = rexRes.group(2)
def __repr__(self):
res = '<Style %s|family %s' % (self.name, self.family)
if self.displayName != None: res += '|displayName "%s"'%self.displayName
if self.styleClass != None: res += '|class %s' % self.styleClass
if self.fontSize != None:
res += '|fontSize %d%s' % (self.fontSize, self.fontSizeUnit)
if self.outlineLevel != None: res += '|level %s' % self.outlineLevel
return ('%s>' % res).encode('utf-8')
# ------------------------------------------------------------------------------
class Styles(UserDict):
def getParagraphStyleAtLevel(self, level):
'''Tries to find a style which has level p_level. Returns None if no
such style exists.'''
res = None
for style in self.itervalues():
if (style.family == 'paragraph') and (style.outlineLevel == level):
res = style
break
return res
def getStyle(self, displayName):
'''Gets the style that has this p_displayName. Returns None if not
found.'''
res = None
for style in self.itervalues():
if style.displayName == displayName:
res = style
break
return res
def getStyles(self, stylesType='all'):
'''Returns a list of all the styles of the given p_stylesType.'''
res = []
if stylesType == 'all':
res = self.values()
else:
for style in self.itervalues():
if (style.family == stylesType) and style.displayName:
res.append(style)
return res
# ------------------------------------------------------------------------------
class StylesEnvironment(OdfEnvironment):
def __init__(self):
OdfEnvironment.__init__(self)
self.styles = Styles()
self.state = READING
self.currentStyle = None # The style definition currently parsed
# ------------------------------------------------------------------------------
class StylesParser(OdfParser):
def __init__(self, env, caller):
OdfParser.__init__(self, env, caller)
self.styleTag = None
def endDocument(self):
e = OdfParser.endDocument(self)
self.caller.styles = e.styles
def startElement(self, elem, attrs):
e = OdfParser.startElement(self, elem, attrs)
self.styleTag = '%s:style' % e.ns(e.NS_STYLE)
if elem == self.styleTag:
e.state = PARSING_STYLE
nameAttr = '%s:name' % e.ns(e.NS_STYLE)
familyAttr = '%s:family' % e.ns(e.NS_STYLE)
classAttr = '%s:class' % e.ns(e.NS_STYLE)
displayNameAttr = '%s:display-name' % e.ns(e.NS_STYLE)
# Create the style
style = Style(name=attrs[nameAttr], family=attrs[familyAttr])
if attrs.has_key(classAttr):
style.styleClass = attrs[classAttr]
if attrs.has_key(displayNameAttr):
style.displayName = attrs[displayNameAttr]
# Record this style in the environment
e.styles[style.name] = style
e.currentStyle = style
outlineLevelKey = '%s:default-outline-level' % e.ns(e.NS_STYLE)
if attrs.has_key(outlineLevelKey):
style.outlineLevel = int(attrs[outlineLevelKey])
else:
if e.state == PARSING_STYLE:
# I am parsing tags within the style.
if elem == ('%s:text-properties' % e.ns(e.NS_STYLE)):
fontSizeKey = '%s:font-size' % e.ns(e.NS_FO)
if attrs.has_key(fontSizeKey):
e.currentStyle.setFontSize(attrs[fontSizeKey])
def endElement(self, elem):
e = OdfParser.endElement(self, elem)
if elem == self.styleTag:
e.state = READING
e.currentStyle = None
# -------------------------------------------------------------------------------
class StylesManager:
'''Reads the paragraph styles from styles.xml within an ODT file, and
updates styles.xml with some predefined POD styles.'''
podSpecificStyles = {
'podItemKeepWithNext': Style('podItemKeepWithNext', 'paragraph'),
# This style is common to bullet and number items. Behing the scenes,
# there are 2 concrete ODT styles: podBulletItemKeepWithNext and
# podNumberItemKeepWithNext. pod chooses the right one.
}
def __init__(self, stylesString):
self.stylesString = stylesString
self.styles = None
# Global styles mapping
self.stylesMapping = None
self.stylesParser = StylesParser(StylesEnvironment(), self)
self.stylesParser.parse(self.stylesString)
# Now self.styles contains the styles.
# List of text styles derived from self.styles
self.textStyles = self.styles.getStyles('text')
# List of paragraph styles derived from self.styles
self.paragraphStyles = self.styles.getStyles('paragraph')
def checkStylesAdequation(self, htmlStyle, odtStyle):
'''Checks that p_odtStyle my be used for style p_htmlStyle.'''
if (htmlStyle in XHTML_PARAGRAPH_TAGS_NO_LISTS) and \
(odtStyle in self.textStyles):
raise PodError(
HTML_PARA_ODT_TEXT % (htmlStyle, odtStyle.displayName))
if (htmlStyle in XHTML_INNER_TAGS) and \
(odtStyle in self.paragraphStyles):
raise PodError(HTML_TEXT_ODT_PARA % (
htmlStyle, odtStyle.displayName))
def checkStylesMapping(self, stylesMapping):
'''Checks that the given p_stylesMapping is correct. Returns the same
dict as p_stylesMapping, but with Style instances as values, instead
of strings (style's display names).'''
res = {}
if not isinstance(stylesMapping, dict) and \
not isinstance(stylesMapping, UserDict):
raise PodError(MAPPING_NOT_DICT)
for xhtmlStyleName, odtStyleName in stylesMapping.iteritems():
if not isinstance(xhtmlStyleName, basestring):
raise PodError(MAPPING_ELEM_NOT_STRING)
if (xhtmlStyleName == 'h*') and \
not isinstance(odtStyleName, int):
raise PodError(MAPPING_OUTLINE_DELTA_NOT_INT)
if (xhtmlStyleName != 'h*') and \
not isinstance(odtStyleName, basestring):
raise PodError(MAPPING_ELEM_NOT_STRING)
if (xhtmlStyleName != 'h*') and \
((not xhtmlStyleName) or (not odtStyleName)):
raise PodError(MAPPING_ELEM_EMPTY)
if xhtmlStyleName in XHTML_UNSTYLABLE_TAGS:
raise PodError(UNSTYLABLE_TAG % (xhtmlStyleName,
XHTML_UNSTYLABLE_TAGS))
if xhtmlStyleName != 'h*':
odtStyle = self.styles.getStyle(odtStyleName)
if not odtStyle:
if self.podSpecificStyles.has_key(odtStyleName):
odtStyle = self.podSpecificStyles[odtStyleName]
else:
raise PodError(STYLE_NOT_FOUND % odtStyleName)
self.checkStylesAdequation(xhtmlStyleName, odtStyle)
res[xhtmlStyleName] = odtStyle
else:
res[xhtmlStyleName] = odtStyleName # In this case, it is the
# outline level, not an ODT style name
return res
# ------------------------------------------------------------------------------

16
pod/test/Readme.txt Executable file
View file

@ -0,0 +1,16 @@
Here you will find some ODT documents that are POD templates.
A POD template is a standard ODT file, where:
- notes are used to insert Python-based code for telling POD to render
a portion of the document zero, one or more times ("if" and "for" statements);
- text insertions in "track changes" mode are interpreted as Python expressions.
When you invoke the tester.py program with one of those ODT files as unique parameter
(ie "python tester.py ForCellOnlyOne.odt"), you get a result.odt file which is the
result of executing the template with a bunch of Python objects. The "tests" dictionary
defined in tester.py contains the objects that are given to each POD ODT template
contained in this folder.
Opening the templates with OpenOffice (2.0 or higher), running tester.py on it and
checking the result in result.odt is probably the quickest way to have a good idea
of what appy.pod can make for you !

223
pod/test/Tester.py Executable file
View file

@ -0,0 +1,223 @@
# ------------------------------------------------------------------------------
# Appy is a framework for building applications in the Python language.
# Copyright (C) 2007 Gaetan Delannay
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,USA.
# ------------------------------------------------------------------------------
import os, os.path, sys, zipfile, re, shutil
import appy.shared.test
from appy.shared.test import TesterError
from appy.shared.utils import FolderDeleter
from appy.pod.odf_parser import OdfEnvironment, OdfParser
from appy.pod.renderer import Renderer
from appy.pod import XML_SPECIAL_CHARS
# TesterError-related constants ------------------------------------------------
TEMPLATE_NOT_FOUND = 'Template file "%s" was not found.'
CONTEXT_NOT_FOUND = 'Context file "%s" was not found.'
EXPECTED_RESULT_NOT_FOUND = 'Expected result "%s" was not found.'
# ------------------------------------------------------------------------------
class AnnotationsRemover(OdfParser):
'''This parser is used to remove from content.xml and styles.xml the
Python tracebacks that may be dumped into OpenDocument annotations by
pod when generating errors. Indeed, those tracebacks contain lot of
machine-specific info, like absolute paths to the python files, etc.'''
def __init__(self, env, caller):
OdfParser.__init__(self, env, caller)
self.res = u''
self.inAnnotation = False # Are we parsing an annotation ?
self.textEncountered = False # Within an annotation, have we already
# met a text ?
self.ignore = False # Must we avoid dumping the current tag/content
# into the result ?
def startElement(self, elem, attrs):
e = OdfParser.startElement(self, elem, attrs)
# Do we enter into an annotation ?
if elem == '%s:annotation' % e.ns(e.NS_OFFICE):
self.inAnnotation = True
self.textEncountered = False
elif elem == '%s:p' % e.ns(e.NS_TEXT):
if self.inAnnotation:
if not self.textEncountered:
self.textEncountered = True
else:
self.ignore = True
if not self.ignore:
self.res += '<%s' % elem
for attrName, attrValue in attrs.items():
self.res += ' %s="%s"' % (attrName, attrValue)
self.res += '>'
def endElement(self, elem):
e = OdfParser.endElement(self, elem)
if elem == '%s:annotation' % e.ns(e.NS_OFFICE):
self.inAnnotation = False
self.ignore = False
if not self.ignore:
self.res += '</%s>' % elem
def characters(self, content):
e = OdfParser.characters(self, content)
if not self.ignore:
for c in content:
if XML_SPECIAL_CHARS.has_key(c):
self.res += XML_SPECIAL_CHARS[c]
else:
self.res += c
def getResult(self):
return self.res
# ------------------------------------------------------------------------------
class Test(appy.shared.test.Test):
'''Abstract test class.'''
interestingOdtContent = ('content.xml', 'styles.xml')
def __init__(self, testData, testDescription, testFolder, config, flavour):
appy.shared.test.Test.__init__(self, testData, testDescription,
testFolder, config, flavour)
self.templatesFolder = os.path.join(self.testFolder, 'templates')
self.contextsFolder = os.path.join(self.testFolder, 'contexts')
self.resultsFolder = os.path.join(self.testFolder, 'results')
self.result = None
def getContext(self, contextName):
'''Gets the objects that are in the context.'''
contextPy = os.path.join(self.contextsFolder, contextName + '.py')
if not os.path.exists(contextPy):
raise TesterError(CONTEXT_NOT_FOUND % contextPy)
contextPkg = 'appy.pod.test.contexts.%s' % contextName
exec 'import %s' % contextPkg
exec 'context = dir(%s)' % contextPkg
res = {}
for elem in context:
if not elem.startswith('__'):
exec 'res[elem] = %s.%s' % (contextPkg, elem)
return res
def do(self):
self.result = os.path.join(
self.tempFolder, '%s.%s' % (
self.data['Name'], self.data['Result']))
# Get the path to the template to use for this test
template = os.path.join(self.templatesFolder,
self.data['Template'] + '.odt')
if not os.path.exists(template):
raise TesterError(TEMPLATE_NOT_FOUND % template)
# Get the context
context = self.getContext(self.data['Context'])
# Get the OpenOffice port
ooPort = self.data['OpenOfficePort']
pythonWithUno = self.config['pythonWithUnoPath']
# Get the styles mapping
stylesMapping = eval('{' + self.data['StylesMapping'] + '}')
# Mmh, dicts are not yet managed by RtfTablesParser
# Call the renderer.
Renderer(template, context, self.result, ooPort=ooPort,
pythonWithUnoPath=pythonWithUno,
stylesMapping=stylesMapping).run()
# Store all result files
# I should allow to do this from an option given to Tester.py: this code
# keeps in a separate folder the odt results of all ran tests.
#tempFolder2 = '%s/sevResults' % self.testFolder
#if not os.path.exists(tempFolder2):
# os.mkdir(tempFolder2)
#print 'Result is', self.result, 'temp folder 2 is', tempFolder2
#shutil.copy(self.result, tempFolder2)
def getOdtContent(self, odtFile):
'''Creates in the temp folder content.xml and styles.xml extracted
from p_odtFile.'''
contentXml = None
stylesXml = None
if odtFile == self.result:
filePrefix = 'actual'
else:
filePrefix = 'expected'
zipFile = zipfile.ZipFile(odtFile)
for zippedFile in zipFile.namelist():
if zippedFile in self.interestingOdtContent:
f = file(os.path.join(self.tempFolder,
'%s.%s' % (filePrefix, zippedFile)), 'wb')
fileContent = zipFile.read(zippedFile)
if zippedFile == 'content.xml':
# Sometimes, in annotations, there are Python tracebacks.
# Those tracebacks include the full path to the Python
# files, which of course may be different from one machine
# to the other. So we remove those paths.
annotationsRemover = AnnotationsRemover(
OdfEnvironment(), self)
annotationsRemover.parse(fileContent)
fileContent = annotationsRemover.getResult()
f.write(fileContent.encode('utf-8'))
f.close()
zipFile.close()
def checkResult(self):
'''r_ is False if the test succeeded.'''
# Get styles.xml and content.xml from the actual result
res = False
self.getOdtContent(self.result)
# Get styles.xml and content.xml from the expected result
expectedResult = os.path.join(self.resultsFolder,
self.data['Name'] + '.odt')
if not os.path.exists(expectedResult):
raise TesterError(EXPECTED_RESULT_NOT_FOUND % expectedResult)
self.getOdtContent(expectedResult)
for fileName in self.interestingOdtContent:
diffOccurred = self.compareFiles(
os.path.join(self.tempFolder, 'actual.%s' % fileName),
os.path.join(self.tempFolder, 'expected.%s' % fileName),
areXml=True, xmlTagsToIgnore=(
(OdfEnvironment.NS_DC, 'date'),
(OdfEnvironment.NS_STYLE, 'style')),
xmlAttrsToIgnore=('draw:name','text:name'), encoding='utf-8')
if diffOccurred:
res = True
break
return res
# Concrete test classes --------------------------------------------------------
class NominalTest(Test):
'''Tests an application model.'''
def __init__(self, testData, testDescription, testFolder, config, flavour):
Test.__init__(self, testData, testDescription, testFolder, config,
flavour)
class ErrorTest(Test):
'''Tests an application model.'''
def __init__(self, testData, testDescription, testFolder, config, flavour):
Test.__init__(self, testData, testDescription, testFolder, config,
flavour)
def onError(self):
'''Compares the error that occurred with the expected error.'''
Test.onError(self)
return not self.isExpectedError(self.data['Message'])
# ------------------------------------------------------------------------------
class PodTestFactory(appy.shared.test.TestFactory):
def createTest(testData, testDescription, testFolder, config, flavour):
if testData.table.instanceOf('ErrorTest'):
test = ErrorTest(testData, testDescription, testFolder, config,
flavour)
else:
test = NominalTest(testData, testDescription, testFolder, config,
flavour)
return test
createTest = staticmethod(createTest)
# ------------------------------------------------------------------------------
class PodTester(appy.shared.test.Tester):
def __init__(self, testPlan):
appy.shared.test.Tester.__init__(self, testPlan, [], PodTestFactory)
# ------------------------------------------------------------------------------
if __name__ == '__main__':
PodTester('Tests.rtf').run()
# ------------------------------------------------------------------------------

1688
pod/test/Tests.rtf Executable file

File diff suppressed because it is too large Load diff

1
pod/test/__init__.py Executable file
View file

@ -0,0 +1 @@

View file

@ -0,0 +1,10 @@
trueCondition = True
falseCondition = False
class O:
def __init__(self, v):
self.v = v
self.vv = v+v
oooo = [O('a'), O('b'), O('c'), O('d')]

1
pod/test/contexts/Empty.py Executable file
View file

@ -0,0 +1 @@
# This file is really empty.

View file

@ -0,0 +1,6 @@
import os.path
import appy
def getFileHandler():
return file('%s/pod/test/templates/NoPython.odt' % os.path.dirname(appy.__file__))

View file

@ -0,0 +1,3 @@
from appy.pod.test.contexts import Group
groups = [Group('group1'), Group('group2'), Group('toto')]

View file

@ -0,0 +1,6 @@
import os.path
import appy
def getAppyPath():
return os.path.dirname(appy.__file__)

View file

@ -0,0 +1,3 @@
expr1 = 'hello'
i1 = 45
f1 = 78.05

View file

@ -0,0 +1,6 @@
import os.path
import appy
def getAppyPath():
return os.path.dirname(appy.__file__)

View file

@ -0,0 +1,4 @@
from appy.pod.test.contexts import Person
persons = [Person('P1'), Person('P2'), Person('P3'), Person('P4'),
Person('P5'), Person('P6'), Person('P7'), Person('P8')]

View file

@ -0,0 +1,3 @@
from appy.pod.test.contexts import Person
persons = [Person('P1'), Person('P2'), Person('P3'), Person('P4')]

View file

@ -0,0 +1,3 @@
from appy.pod.test.contexts import Person
persons = [Person('P1'), Person('P2'), Person('P3')]

View file

@ -0,0 +1,3 @@
from appy.pod.test.contexts import Person
persons = [Person('P1'), Person('P2')]

View file

@ -0,0 +1 @@
list1 = []

View file

@ -0,0 +1 @@
list1 = ['Hello', 'World', 45, True]

View file

@ -0,0 +1,3 @@
from appy.pod.test.contexts import Person
persons = [Person('Mr 1'), Person('Ms One'), Person('Misss two')]

View file

@ -0,0 +1 @@
c1 = False

View file

@ -0,0 +1 @@
c1 = True

View file

@ -0,0 +1,2 @@
IWillTellYouWhatInAMoment = 'return'
beingPaidForIt = True

View file

@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
xhtmlInput = '''
<p>Te<b>s</b>t1 : <b>bold</b>, i<i>tal</i>ics, exponent<sup>34</sup>, sub<sub>45</sub>.</p>
<p>An <a href="http://www.google.com">hyperlink</a> to Google.</p>
<ol><li>Number list, item 1</li>
<ol><li>Sub-item 1</li><li>Sub-Item 2</li>
<ol><li>Sub-sub-item A</li><li>Sub-sub-item B <i>italic</i>.</li></ol>
</ol>
</ol>
<ul><li>A bullet</li>
<ul><li>A sub-bullet</li>
<ul><li>A sub-sub-bullet</li></ul>
<ol><li>A sub-sub number</li><li>Another.<br /></li></ol>
</ul>
</ul>
<h2>Heading<br /></h2>
Heading Blabla.<br />
<h3>SubHeading</h3>
Subheading blabla.<br />
'''
# I need a class.
class D:
def getAt1(self):
return xhtmlInput
dummy = D()

View file

@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
xhtmlInput = '''
<div><strong>Programmes FSE Convergence et Compétitivité
régionale et emploi.</strong></div>'''
xhtmlInput2 = '''<b>Walloon entreprises, welcome !</b><br/>
<br/>
This site will allow you to get simple answers to those questions:<br/>
- am I an SME or not ?<br/>
- to which incentives may I postulate for, in Wallonia, according to my size?
<br/>The little test which you will find on this site is based on the European
Recommendation of May 6th, 2003. It was enforced on January 1st, 2005.
Most of the incentives that are available for SMEs in Wallonia are based
on the SME definition proposed by this recommandation.<br/><br/>
Incentives descriptions come from the
<a href="http://economie.wallonie.be/" target="_blank">MIDAS</a>
database and represent all incentives that are available on the Walloon
territory, whatever public institution is proposing it.<br/><br/>
<b>Big enterprises, do not leave !</b><br/><br/>
If this sites classifies you as a big enterprise, you will be able to consult
all incentives that are targeted to you.'''
xhtmlInput3 = '''
<div><strong>Programmes A</strong></div>
<div>Programmes B</div>
<div><strong>Programmes C</strong></div>
<ul><li>a</li><li>b</li></ul>'''

View file

@ -0,0 +1,57 @@
# -*- coding: utf-8 -*-
xhtmlInput = '''
<p>
<table class="plain">
<thead>
<tr>
<th class="align-right" align="right">Title column one<br /></th>
<th>title column two</th>
</tr>
</thead>
<tbody>
<tr>
<td class="align-right" align="right">Hi with a <a class="generated" href="http://easi.wallonie.be">http://easi.wallonie.be</a> <br /></td>
<td>fdff</td>
</tr>
<tr>
<td class="align-right" align="right"><br /></td>
<td><br /></td>
</tr>
<tr>
<td class="align-right" align="left">Some text here<br />
<ul><li>Bullet One</li><li>Bullet Two</li>
<ul><li>Sub-bullet A</li><li>Sub-bullet B</li>
<ul><li>Subsubboulette<br /></li></ul>
</ul>
</ul>
</td>
<td>
<table>
<tbody>
<tr>
<td>SubTable</td>
<td>Columns 2<br /></td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<br /></p>
'''
xhtmlInput2 = '''
<ul><li>
<p>a</p>
</li><li>
<p>b</p>
</li><li>
<p>c</p>
</li>
<ul>
<li><p>SUB</p>
</li>
</ul>
</ul>
'''

View file

@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
xhtmlInput = '''
<p>Some HTML entities: é: &eacute;, è: &egrave;, Atilde: &Atilde;.</p>
<p>XML entities: amp: &amp;, quote: &quot;, apos: &apos;, lt: &lt;, gt: &gt;.</p>'''

View file

@ -0,0 +1,44 @@
# -*- coding: utf-8 -*-
# I need a class.
class D:
def getAt1(self):
return '''
<p>Notifia</p>
<ol>
<li>Een</li>
<li>Een</li>
<li>Een</li>
<li class="podItemKeepWithNext">Keep with next, without style mapping.</li>
<li>Een</li>
<li>Een</li>
<li>Een</li>
<li>Een</li>
<li>Een</li>
<li>Een</li>
<li>Een</li>
<ul><li class="pmItemKeepWithNext">This one has 'keep with next'</li>
<li>Hello</li>
<ol><li>aaaaaaaaaa aaaaaaaaaaaaaa</li>
<li>aaaaaaaaaa aaaaaaaaaaaaaa</li>
<li>aaaaaaaaaa aaaaaaaaaaaaaa</li>
<li class="pmItemKeepWithNext">This one has 'keep with next'</li>
</ol>
</ul>
<li>Een</li>
<li>Een</li>
<li>Een</li>
<li>Een</li>
<li>Een</li>
<li>Een</li>
<li class="pmItemKeepWithNext">This one has 'keep with next'</li>
</ol>
<ul>
<li>Un</li>
<li>Deux</li>
<li>Trois</li>
<li>Quatre</li>
<li class="pmItemKeepWithNext">VCinq (this one has 'keep with next')</li>
<li class="pmItemKeepWithNext">Six (this one has 'keep with next')</li>
<li class="pmItemKeepWithNext">Sept (this one has 'keep with next')</li>
</ul>'''
dummy = D()

View file

@ -0,0 +1,6 @@
# -*- coding: utf-8 -*-
# I need a class.
class D:
def getAt1(self):
return '\n<p>Test1<br /></p>\n'
dummy = D()

View file

@ -0,0 +1,12 @@
# -*- coding: utf-8 -*-
xhtmlInput = '''
<p>Hello.</p>
<h2>Heading One</h2>
Blabla.<br />
<h3>SubHeading then.</h3>
Another blabla.<br /><br /><br /> '''
# I need a class.
class D:
def getAt1(self):
return xhtmlInput
dummy = D()

View file

@ -0,0 +1,12 @@
# -*- coding: utf-8 -*-
xhtmlInput = '''
<p>Hello.</p>
<h2>Heading One</h2>
Blabla.<br />
<h3>SubHeading then.</h3>
Another blabla.<br /><br /><br /> '''
# I need a class.
class D:
def getAt1(self):
return xhtmlInput
dummy = D()

View file

@ -0,0 +1,35 @@
# -*- coding: utf-8 -*-
xhtmlInput = '''
<p>Table test.</p>
<p>
<table class="plain">
<tbody>
<tr>
<td>Table 1 <br /></td>
<td colspan="2">aaaaa<br /></td>
</tr>
<tr>
<td>zzz <br /></td>
<td>
<table>
<tr>
<td>SubTableA</td>
<td>SubTableB</td>
</tr>
<tr>
<td>SubTableC</td>
<td>SubTableD</td>
</tr>
</table>
</td>
<td><b>Hello</b> blabla<table><tr><td>SubTableOneRowOneColumn</td></tr></table></td>
</tr>
<tr>
<td><p>Within a <b>para</b>graph</p></td>
<td><b>Hello</b> non bold</td>
<td>Hello <b>bold</b> not bold</td>
</tr>
</tbody>
</table>
</p>
<br />'''

18
pod/test/contexts/__init__.py Executable file
View file

@ -0,0 +1,18 @@
# Here I define some classes that will be used for defining objects in several
# contexts.
class Person:
def __init__(self, name):
self.name = name
self.lastName = '%s last name' % name
self.firstName = '%s first name' % name
self.address = '%s address' % name
class Group:
def __init__(self, name):
self.name = name
if name == 'group1':
self.persons = [Person('P1'), Person('P2'), Person('P3')]
elif name == 'group2':
self.persons = [Person('RA'), Person('RB')]
else:
self.persons = []

BIN
pod/test/images/linux.jpg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
pod/test/images/plone.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
pod/test/images/python.gif Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Binary file not shown.

BIN
pod/test/results/errorFooter.odt Executable file

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
pod/test/results/errorIf.odt Executable file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
pod/test/results/forTable.odt Executable file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
pod/test/results/headerFooter.odt Executable file

Binary file not shown.

BIN
pod/test/results/ifAndFors1.odt Executable file

Binary file not shown.

BIN
pod/test/results/ifElseErrors.odt Executable file

Binary file not shown.

BIN
pod/test/results/imagesImport.odt Executable file

Binary file not shown.

BIN
pod/test/results/noPython.odt Executable file

Binary file not shown.

Binary file not shown.

BIN
pod/test/results/pathImport.odt Executable file

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
pod/test/results/simpleForRow.odt Executable file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
pod/test/results/simpleTest.odt Executable file

Binary file not shown.

BIN
pod/test/results/withAnImage.odt Executable file

Binary file not shown.

BIN
pod/test/results/xhtmlComplex.odt Executable file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
pod/test/results/xhtmlNominal.odt Executable file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
pod/test/templates/ErrorIf.odt Executable file

Binary file not shown.

Binary file not shown.

BIN
pod/test/templates/ForCell.odt Executable file

Binary file not shown.

BIN
pod/test/templates/ForCell2.odt Executable file

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show more