Initial import
This commit is contained in:
commit
4043163fc4
427 changed files with 18387 additions and 0 deletions
82
pod/__init__.py
Executable file
82
pod/__init__.py
Executable 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 = {'<': '<', '>': '>', '&': '&', '"': '"',
|
||||
"'": '''}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
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
205
pod/actions.py
Executable 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
501
pod/buffers.py
Executable 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
222
pod/converter.py
Executable 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
230
pod/doc_importers.py
Executable 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
87
pod/elements.py
Executable 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
51
pod/odf_parser.py
Executable 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
303
pod/pod_parser.py
Executable 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
447
pod/renderer.py
Executable 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
107
pod/styles.in.content.xml
Executable 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
6
pod/styles.in.styles.xml
Executable 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
238
pod/styles_manager.py
Executable 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
16
pod/test/Readme.txt
Executable 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
223
pod/test/Tester.py
Executable 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
1688
pod/test/Tests.rtf
Executable file
File diff suppressed because it is too large
Load diff
1
pod/test/__init__.py
Executable file
1
pod/test/__init__.py
Executable file
|
@ -0,0 +1 @@
|
|||
|
10
pod/test/contexts/ElseStatements.py
Executable file
10
pod/test/contexts/ElseStatements.py
Executable 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
1
pod/test/contexts/Empty.py
Executable file
|
@ -0,0 +1 @@
|
|||
# This file is really empty.
|
6
pod/test/contexts/FileHandlerImport.py
Executable file
6
pod/test/contexts/FileHandlerImport.py
Executable 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__))
|
||||
|
3
pod/test/contexts/IfAndFors1.py
Executable file
3
pod/test/contexts/IfAndFors1.py
Executable file
|
@ -0,0 +1,3 @@
|
|||
from appy.pod.test.contexts import Group
|
||||
|
||||
groups = [Group('group1'), Group('group2'), Group('toto')]
|
6
pod/test/contexts/ImagesImport.py
Executable file
6
pod/test/contexts/ImagesImport.py
Executable file
|
@ -0,0 +1,6 @@
|
|||
import os.path
|
||||
import appy
|
||||
|
||||
def getAppyPath():
|
||||
return os.path.dirname(appy.__file__)
|
||||
|
3
pod/test/contexts/OnlyExpressions.py
Executable file
3
pod/test/contexts/OnlyExpressions.py
Executable file
|
@ -0,0 +1,3 @@
|
|||
expr1 = 'hello'
|
||||
i1 = 45
|
||||
f1 = 78.05
|
6
pod/test/contexts/PathImport.py
Executable file
6
pod/test/contexts/PathImport.py
Executable file
|
@ -0,0 +1,6 @@
|
|||
import os.path
|
||||
import appy
|
||||
|
||||
def getAppyPath():
|
||||
return os.path.dirname(appy.__file__)
|
||||
|
4
pod/test/contexts/PersonsEight.py
Executable file
4
pod/test/contexts/PersonsEight.py
Executable 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')]
|
3
pod/test/contexts/PersonsFour.py
Executable file
3
pod/test/contexts/PersonsFour.py
Executable file
|
@ -0,0 +1,3 @@
|
|||
from appy.pod.test.contexts import Person
|
||||
|
||||
persons = [Person('P1'), Person('P2'), Person('P3'), Person('P4')]
|
3
pod/test/contexts/PersonsThree.py
Executable file
3
pod/test/contexts/PersonsThree.py
Executable file
|
@ -0,0 +1,3 @@
|
|||
from appy.pod.test.contexts import Person
|
||||
|
||||
persons = [Person('P1'), Person('P2'), Person('P3')]
|
3
pod/test/contexts/PersonsTwo.py
Executable file
3
pod/test/contexts/PersonsTwo.py
Executable file
|
@ -0,0 +1,3 @@
|
|||
from appy.pod.test.contexts import Person
|
||||
|
||||
persons = [Person('P1'), Person('P2')]
|
1
pod/test/contexts/SimpleForEmptyList.py
Executable file
1
pod/test/contexts/SimpleForEmptyList.py
Executable file
|
@ -0,0 +1 @@
|
|||
list1 = []
|
1
pod/test/contexts/SimpleForFilledList.py
Executable file
1
pod/test/contexts/SimpleForFilledList.py
Executable file
|
@ -0,0 +1 @@
|
|||
list1 = ['Hello', 'World', 45, True]
|
3
pod/test/contexts/SimpleForRow.py
Executable file
3
pod/test/contexts/SimpleForRow.py
Executable file
|
@ -0,0 +1,3 @@
|
|||
from appy.pod.test.contexts import Person
|
||||
|
||||
persons = [Person('Mr 1'), Person('Ms One'), Person('Misss two')]
|
1
pod/test/contexts/SimpleIfIsFalse.py
Executable file
1
pod/test/contexts/SimpleIfIsFalse.py
Executable file
|
@ -0,0 +1 @@
|
|||
c1 = False
|
1
pod/test/contexts/SimpleIfIsTrue.py
Executable file
1
pod/test/contexts/SimpleIfIsTrue.py
Executable file
|
@ -0,0 +1 @@
|
|||
c1 = True
|
2
pod/test/contexts/SimpleTest.py
Executable file
2
pod/test/contexts/SimpleTest.py
Executable file
|
@ -0,0 +1,2 @@
|
|||
IWillTellYouWhatInAMoment = 'return'
|
||||
beingPaidForIt = True
|
25
pod/test/contexts/XhtmlComplex.py
Executable file
25
pod/test/contexts/XhtmlComplex.py
Executable 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()
|
30
pod/test/contexts/XhtmlComplex2.py
Executable file
30
pod/test/contexts/XhtmlComplex2.py
Executable 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>'''
|
57
pod/test/contexts/XhtmlComplexTables.py
Executable file
57
pod/test/contexts/XhtmlComplexTables.py
Executable 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>
|
||||
'''
|
4
pod/test/contexts/XhtmlEntities.py
Executable file
4
pod/test/contexts/XhtmlEntities.py
Executable file
|
@ -0,0 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
xhtmlInput = '''
|
||||
<p>Some HTML entities: é: é, è: è, Atilde: Ã.</p>
|
||||
<p>XML entities: amp: &, quote: ", apos: ', lt: <, gt: >.</p>'''
|
44
pod/test/contexts/XhtmlKeepWithNext.py
Executable file
44
pod/test/contexts/XhtmlKeepWithNext.py
Executable 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()
|
6
pod/test/contexts/XhtmlNominal.py
Executable file
6
pod/test/contexts/XhtmlNominal.py
Executable 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()
|
12
pod/test/contexts/XhtmlStylesErrors.py
Executable file
12
pod/test/contexts/XhtmlStylesErrors.py
Executable 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()
|
12
pod/test/contexts/XhtmlStylesMapping.py
Executable file
12
pod/test/contexts/XhtmlStylesMapping.py
Executable 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()
|
35
pod/test/contexts/XhtmlTables.py
Executable file
35
pod/test/contexts/XhtmlTables.py
Executable 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
18
pod/test/contexts/__init__.py
Executable 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
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
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
BIN
pod/test/images/python.gif
Executable file
Binary file not shown.
After Width: | Height: | Size: 1.2 KiB |
BIN
pod/test/results/elseStatements.odt
Executable file
BIN
pod/test/results/elseStatements.odt
Executable file
Binary file not shown.
BIN
pod/test/results/errorExpression.odt
Executable file
BIN
pod/test/results/errorExpression.odt
Executable file
Binary file not shown.
BIN
pod/test/results/errorFooter.odt
Executable file
BIN
pod/test/results/errorFooter.odt
Executable file
Binary file not shown.
BIN
pod/test/results/errorForParsetime.odt
Executable file
BIN
pod/test/results/errorForParsetime.odt
Executable file
Binary file not shown.
BIN
pod/test/results/errorForRuntime.odt
Executable file
BIN
pod/test/results/errorForRuntime.odt
Executable file
Binary file not shown.
BIN
pod/test/results/errorIf.odt
Executable file
BIN
pod/test/results/errorIf.odt
Executable file
Binary file not shown.
BIN
pod/test/results/fileHandlerImport.odt
Executable file
BIN
pod/test/results/fileHandlerImport.odt
Executable file
Binary file not shown.
BIN
pod/test/results/forCellCorrectNumber.odt
Executable file
BIN
pod/test/results/forCellCorrectNumber.odt
Executable file
Binary file not shown.
BIN
pod/test/results/forCellNotEnough.odt
Executable file
BIN
pod/test/results/forCellNotEnough.odt
Executable file
Binary file not shown.
BIN
pod/test/results/forCellOnlyOne.odt
Executable file
BIN
pod/test/results/forCellOnlyOne.odt
Executable file
Binary file not shown.
BIN
pod/test/results/forCellTooMuch1.odt
Executable file
BIN
pod/test/results/forCellTooMuch1.odt
Executable file
Binary file not shown.
BIN
pod/test/results/forCellTooMuch2.odt
Executable file
BIN
pod/test/results/forCellTooMuch2.odt
Executable file
Binary file not shown.
BIN
pod/test/results/forCellTooMuch3.odt
Executable file
BIN
pod/test/results/forCellTooMuch3.odt
Executable file
Binary file not shown.
BIN
pod/test/results/forCellTooMuch4.odt
Executable file
BIN
pod/test/results/forCellTooMuch4.odt
Executable file
Binary file not shown.
BIN
pod/test/results/forTable.odt
Executable file
BIN
pod/test/results/forTable.odt
Executable file
Binary file not shown.
BIN
pod/test/results/forTableMinus.odt
Executable file
BIN
pod/test/results/forTableMinus.odt
Executable file
Binary file not shown.
BIN
pod/test/results/forTableMinus2.odt
Executable file
BIN
pod/test/results/forTableMinus2.odt
Executable file
Binary file not shown.
BIN
pod/test/results/forTableMinusError.odt
Executable file
BIN
pod/test/results/forTableMinusError.odt
Executable file
Binary file not shown.
BIN
pod/test/results/forTableMinusError2.odt
Executable file
BIN
pod/test/results/forTableMinusError2.odt
Executable file
Binary file not shown.
BIN
pod/test/results/headerFooter.odt
Executable file
BIN
pod/test/results/headerFooter.odt
Executable file
Binary file not shown.
BIN
pod/test/results/ifAndFors1.odt
Executable file
BIN
pod/test/results/ifAndFors1.odt
Executable file
Binary file not shown.
BIN
pod/test/results/ifElseErrors.odt
Executable file
BIN
pod/test/results/ifElseErrors.odt
Executable file
Binary file not shown.
BIN
pod/test/results/imagesImport.odt
Executable file
BIN
pod/test/results/imagesImport.odt
Executable file
Binary file not shown.
BIN
pod/test/results/noPython.odt
Executable file
BIN
pod/test/results/noPython.odt
Executable file
Binary file not shown.
BIN
pod/test/results/onlyExpressions.odt
Executable file
BIN
pod/test/results/onlyExpressions.odt
Executable file
Binary file not shown.
BIN
pod/test/results/pathImport.odt
Executable file
BIN
pod/test/results/pathImport.odt
Executable file
Binary file not shown.
BIN
pod/test/results/simpleForEmptyList.odt
Executable file
BIN
pod/test/results/simpleForEmptyList.odt
Executable file
Binary file not shown.
BIN
pod/test/results/simpleForFilledList.odt
Executable file
BIN
pod/test/results/simpleForFilledList.odt
Executable file
Binary file not shown.
BIN
pod/test/results/simpleForRow.odt
Executable file
BIN
pod/test/results/simpleForRow.odt
Executable file
Binary file not shown.
BIN
pod/test/results/simpleFromTest.odt
Executable file
BIN
pod/test/results/simpleFromTest.odt
Executable file
Binary file not shown.
BIN
pod/test/results/simpleIfIsFalse.odt
Executable file
BIN
pod/test/results/simpleIfIsFalse.odt
Executable file
Binary file not shown.
BIN
pod/test/results/simpleIfIsTrue.odt
Executable file
BIN
pod/test/results/simpleIfIsTrue.odt
Executable file
Binary file not shown.
BIN
pod/test/results/simpleIfIsTrue003.odt
Executable file
BIN
pod/test/results/simpleIfIsTrue003.odt
Executable file
Binary file not shown.
BIN
pod/test/results/simpleMinusError.odt
Executable file
BIN
pod/test/results/simpleMinusError.odt
Executable file
Binary file not shown.
BIN
pod/test/results/simpleTest.odt
Executable file
BIN
pod/test/results/simpleTest.odt
Executable file
Binary file not shown.
BIN
pod/test/results/withAnImage.odt
Executable file
BIN
pod/test/results/withAnImage.odt
Executable file
Binary file not shown.
BIN
pod/test/results/xhtmlComplex.odt
Executable file
BIN
pod/test/results/xhtmlComplex.odt
Executable file
Binary file not shown.
BIN
pod/test/results/xhtmlComplex2.odt
Normal file
BIN
pod/test/results/xhtmlComplex2.odt
Normal file
Binary file not shown.
BIN
pod/test/results/xhtmlComplexTables.odt
Executable file
BIN
pod/test/results/xhtmlComplexTables.odt
Executable file
Binary file not shown.
BIN
pod/test/results/xhtmlEntities.odt
Executable file
BIN
pod/test/results/xhtmlEntities.odt
Executable file
Binary file not shown.
BIN
pod/test/results/xhtmlKeepWithNext.odt
Executable file
BIN
pod/test/results/xhtmlKeepWithNext.odt
Executable file
Binary file not shown.
BIN
pod/test/results/xhtmlNominal.odt
Executable file
BIN
pod/test/results/xhtmlNominal.odt
Executable file
Binary file not shown.
BIN
pod/test/results/xhtmlStylesErrors.odt
Executable file
BIN
pod/test/results/xhtmlStylesErrors.odt
Executable file
Binary file not shown.
BIN
pod/test/results/xhtmlStylesMapping.odt
Executable file
BIN
pod/test/results/xhtmlStylesMapping.odt
Executable file
Binary file not shown.
BIN
pod/test/results/xhtmlTables.odt
Normal file
BIN
pod/test/results/xhtmlTables.odt
Normal file
Binary file not shown.
BIN
pod/test/templates/ElseStatements.odt
Executable file
BIN
pod/test/templates/ElseStatements.odt
Executable file
Binary file not shown.
BIN
pod/test/templates/ErrorExpression.odt
Executable file
BIN
pod/test/templates/ErrorExpression.odt
Executable file
Binary file not shown.
BIN
pod/test/templates/ErrorFooter.odt
Executable file
BIN
pod/test/templates/ErrorFooter.odt
Executable file
Binary file not shown.
BIN
pod/test/templates/ErrorForParsetime.odt
Executable file
BIN
pod/test/templates/ErrorForParsetime.odt
Executable file
Binary file not shown.
BIN
pod/test/templates/ErrorForRuntime.odt
Executable file
BIN
pod/test/templates/ErrorForRuntime.odt
Executable file
Binary file not shown.
BIN
pod/test/templates/ErrorIf.odt
Executable file
BIN
pod/test/templates/ErrorIf.odt
Executable file
Binary file not shown.
BIN
pod/test/templates/FileHandlerImport.odt
Executable file
BIN
pod/test/templates/FileHandlerImport.odt
Executable file
Binary file not shown.
BIN
pod/test/templates/ForCell.odt
Executable file
BIN
pod/test/templates/ForCell.odt
Executable file
Binary file not shown.
BIN
pod/test/templates/ForCell2.odt
Executable file
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
Loading…
Add table
Add a link
Reference in a new issue