moved sources into subdirectory for easier setup

This commit is contained in:
Stefan Klug 2015-10-27 22:36:51 +01:00
parent 4f91a30fec
commit d93f8ce937
190 changed files with 4 additions and 4 deletions

125
appy/__init__.py Normal file
View file

@ -0,0 +1,125 @@
'''Appy allows you to create easily complete applications in Python.'''
# ------------------------------------------------------------------------------
import os.path
# ------------------------------------------------------------------------------
def getPath(): return os.path.dirname(__file__)
def versionIsGreaterThanOrEquals(version):
'''This method returns True if the current Appy version is greater than or
equals p_version. p_version must have a format like "0.5.0".'''
import appy.version
if appy.version.short == 'dev':
# We suppose that a developer knows what he is doing, so we return True.
return True
else:
paramVersion = [int(i) for i in version.split('.')]
currentVersion = [int(i) for i in appy.version.short.split('.')]
return currentVersion >= paramVersion
# ------------------------------------------------------------------------------
class Object:
'''At every place we need an object, but without any requirement on its
class (methods, attributes,...) we will use this minimalist class.'''
def __init__(self, **fields):
for k, v in fields.items():
setattr(self, k, v)
def __repr__(self):
res = '<Object '
for attrName, attrValue in self.__dict__.items():
v = attrValue
if hasattr(v, '__repr__'):
v = v.__repr__()
try:
res += '%s=%s ' % (attrName, v)
except UnicodeDecodeError:
res += '%s=<encoding problem> ' % attrName
res = res.strip() + '>'
return res.encode('utf-8')
def __bool__(self):
return bool(self.__dict__)
def get(self, name, default=None): return getattr(self, name, default)
def __getitem__(self, k): return getattr(self, k)
def update(self, other):
'''Includes information from p_other into p_self.'''
for k, v in other.__dict__.items():
setattr(self, k, v)
def clone(self):
res = Object()
res.update(self)
return res
# ------------------------------------------------------------------------------
class Hack:
'''This class proposes methods for patching some existing code with
alternative methods.'''
@staticmethod
def patch(method, replacement, klass=None):
'''This method replaces m_method with a p_replacement method, but
keeps p_method on its class under name
"_base_<initial_method_name>_". In the patched method, one may use
Hack.base to call the base method. If p_method is static, you must
specify its class in p_klass.'''
# Get the class on which the surgery will take place.
isStatic = klass
klass = klass or method.__self__.__class__
# On this class, store m_method under its "base" name.
name = isStatic and method.__name__ or method.__func__.__name__
baseName = '_base_%s_' % name
if isStatic:
# If "staticmethod" isn't called hereafter, the static functions
# will be wrapped in methods.
method = staticmethod(method)
replacement = staticmethod(replacement)
setattr(klass, baseName, method)
setattr(klass, name, replacement)
@staticmethod
def base(method, klass=None):
'''Allows to call the base (replaced) method. If p_method is static,
you must specify its p_klass.'''
isStatic = klass
klass = klass or method.__self__.__class__
name = isStatic and method.__name__ or method.__func__.__name__
return getattr(klass, '_base_%s_' % name)
@staticmethod
def inject(patchClass, klass, verbose=False):
'''Injects any method or attribute from p_patchClass into klass.'''
patched = []
added = []
for name, attr in patchClass.__dict__.iteritems():
if name.startswith('__'): continue # Ignore special methods
# Unwrap functions from static methods
if attr.__class__.__name__ == 'staticmethod':
attr = attr.__get__(attr)
static = True
else:
static = False
# Is this name already defined on p_klass ?
if hasattr(klass, name):
hasAttr = True
klassAttr = getattr(klass, name)
else:
hasAttr = False
klassAttr = None
if hasAttr and callable(attr) and callable(klassAttr):
# Patch this method via Hack.patch
if static:
Hack.patch(klassAttr, attr, klass)
else:
Hack.patch(klassAttr, attr)
patched.append(name)
else:
# Simply replace the static attr or add the new static
# attribute or method.
setattr(klass, name, attr)
added.append(name)
if verbose:
pName = patchClass.__name__
cName = klass.__name__
print('%d method(s) patched from %s to %s (%s)' % \
(len(patched), pName, cName, str(patched)))
print('%d method(s) and/or attribute(s) added from %s to %s (%s)'%\
(len(added), pName, cName, str(added)))
# ------------------------------------------------------------------------------

89
appy/pod/__init__.py Normal file
View file

@ -0,0 +1,89 @@
# ------------------------------------------------------------------------------
# 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
from appy.shared.xml_parser import escapeXhtml
# 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')
# ------------------------------------------------------------------------------
class PodError(Exception):
@staticmethod
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)
try:
buffer.dumpContent(tLine)
except UnicodeDecodeError:
buffer.dumpContent(tLine.decode('utf-8'))
buffer.write('</%s:p>' % textNs)
@staticmethod
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)
# XXX To remove, present for backward compatibility only
convertToXhtml = escapeXhtml
# ------------------------------------------------------------------------------

385
appy/pod/actions.py Normal file
View file

@ -0,0 +1,385 @@
# ------------------------------------------------------------------------------
# This file is part of Appy, a framework for building applications in the Python
# language. Copyright (C) 2007 Gaetan Delannay
# Appy is free software; you can redistribute it and/or modify it under the
# terms of the GNU General Public License as published by the Free Software
# Foundation; either version 3 of the License, or (at your option) any later
# version.
# Appy 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
# Appy. If not, see <http://www.gnu.org/licenses/>.
# ------------------------------------------------------------------------------
from appy import Object
from appy.pod import PodError
from appy.shared.utils import Traceback
from appy.pod.elements import *
# ------------------------------------------------------------------------------
EVAL_ERROR = 'Error while evaluating expression "%s". %s'
FROM_EVAL_ERROR = 'Error while evaluating the expression "%s" defined in the ' \
'"from" part of a statement. %s'
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 elem(s) must not be dumped
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
# Several actions may co-exist for the same buffer, as a chain of
# BufferAction instances, defined via the following attribute.
self.subAction = None
def getExceptionLine(self, e):
'''Gets the line describing exception p_e, containing the exception
class, message and line number.'''
return '%s: %s' % (e.__class__.__name__, str(e))
def manageError(self, result, context, errorMessage):
'''Manage the encountered error: dump it into the buffer or raise an
exception.'''
if self.buffer.env.raiseOnError:
if not self.buffer.pod:
# Add in the error message the line nb where the errors occurs
# within the PX.
locator = self.buffer.env.parser.locator
# The column number may not be given
col = locator.getColumnNumber()
if col == None: col = ''
else: col = ', column %d' % col
errorMessage += ' (line %s%s)' % (locator.getLineNumber(), col)
# Integrate the traceback (at least, it last lines)
errorMessage += '\n' + Traceback.get(4)
raise Exception(errorMessage)
# Create a temporary buffer to dump the error. If I reuse this buffer to
# dump the error (what I did before), and we are, at some depth, in a
# for loop, this buffer will contain the error message and not the
# content to repeat anymore. It means that this error will also show up
# for every subsequent iteration.
tempBuffer = self.buffer.clone()
PodError.dump(tempBuffer, errorMessage, withinElement=self.elem)
tempBuffer.evaluate(result, context)
def _evalExpr(self, expr, context):
'''Evaluates p_expr with p_context. p_expr can contain an error expr,
in the form "someExpr|errorExpr". If it is the case, if the "normal"
expr raises an error, the "error" expr is evaluated instead.'''
if '|' not in expr:
res = eval(expr, context)
else:
expr, errorExpr = expr.rsplit('|', 1)
try:
res = eval(expr, context)
except Exception:
res = eval(errorExpr, context)
return res
def evaluateExpression(self, result, context, expr):
'''Evaluates expression p_expr with the current p_context. Returns a
tuple (result, errorOccurred).'''
try:
res = self._evalExpr(expr, context)
error = False
except Exception as e:
res = None
errorMessage = EVAL_ERROR % (expr, self.getExceptionLine(e))
self.manageError(result, context, errorMessage)
error = True
return res, error
def execute(self, result, context):
'''Executes this action given some p_context and add the result to
p_result.'''
# 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.manageError(result, context, TABLE_NOT_ONE_CELL % self.expr)
else:
error = False
# Evaluate self.expr in eRes
eRes = None
if self.expr:
eRes,error = self.evaluateExpression(result, context, self.expr)
if not error:
# Trigger action-specific behaviour
self.do(result, context, eRes)
def evaluateBuffer(self, result, context):
if self.source == 'buffer':
self.buffer.evaluate(result, context, removeMainElems=self.minus)
else:
# Evaluate self.fromExpr in feRes
feRes = None
error = False
try:
feRes = eval(self.fromExpr, context)
except Exception as e:
msg = FROM_EVAL_ERROR% (self.fromExpr, self.getExceptionLine(e))
self.manageError(result, context, msg)
error = True
if not error:
result.write(feRes)
def addSubAction(self, action):
'''Adds p_action as a sub-action of this action.'''
if not self.subAction:
self.subAction = action
else:
self.subAction.addSubAction(action)
class IfAction(BufferAction):
'''Action that determines if we must include the content of the buffer in
the result or not.'''
def do(self, result, context, exprRes):
if exprRes:
if self.subAction:
self.subAction.execute(result, context)
else:
self.evaluateBuffer(result, context)
else:
if self.buffer.isMainElement(Cell.OD):
# Don't leave the current row with a wrong number of cells
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, buff, expr, elem, minus, src, fromExpr, ifAction):
IfAction.__init__(self, name, buff, None, elem, minus, src, fromExpr)
self.ifAction = ifAction
def do(self, result, context, exprRes):
# This action is executed if the tied "if" action is not executed.
ifAction = self.ifAction
iRes,error = ifAction.evaluateExpression(result, context, ifAction.expr)
IfAction.do(self, result, context, not iRes)
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, buff, expr, elem, minus, iter, src, fromExpr):
BufferAction.__init__(self, name, buff, expr, elem, minus, src,fromExpr)
self.iter = iter # Name of the iterator variable used in the each loop
def initialiseLoop(self, context, elems):
'''Initialises information about the loop, before entering into it. It
is possible that this loop overrides an outer loop whose iterator
has the same name. This method returns a tuple
(loop, outerOverriddenLoop).'''
# The "loop" object, made available in the POD context, contains info
# about all currently walked loops. For every walked loop, a specific
# object, le'ts name it curLoop, accessible at getattr(loop, self.iter),
# stores info about its status:
# * curLoop.length gives the total number of walked elements withhin
# the loop
# * curLoop.nb gives the index (starting at 0) if the currently
# walked element.
# * curLoop.first is True if the currently walked element is the
# first one.
# * curLoop.last is True if the currently walked element is the
# last one.
# * curLoop.odd is True if the currently walked element is odd
# * curLoop.even is True if the currently walked element is even
# For example, if you have a "for" statement like this:
# for elem in myListOfElements
# Within the part of the ODT document impacted by this statement, you
# may access to:
# * loop.elem.length to know the total length of myListOfElements
# * loop.elem.nb to know the index of the current elem within
# myListOfElements.
if 'loop' not in context:
context['loop'] = Object()
try:
total = len(elems)
except Exception:
total = 0
curLoop = Object(length=total)
# Does this loop overrides an outer loop whose iterator has the same
# name ?
outerLoop = None
if hasattr(context['loop'], self.iter):
outerLoop = getattr(context['loop'], self.iter)
# Put this loop in the global object "loop".
setattr(context['loop'], self.iter, curLoop)
return curLoop, outerLoop
def do(self, result, context, elems):
'''Performs the "for" action. p_elems is the list of elements to
walk, evaluated from self.expr.'''
# Check p_exprRes type
try:
# All "iterable" objects are OK
iter(elems)
except TypeError:
self.manageError(result, context, WRONG_SEQ_TYPE % self.expr)
return
# Remember variable hidden by iter if any
hasHiddenVariable = False
if self.iter in context:
hiddenVariable = context[self.iter]
hasHiddenVariable = True
# In the case of cells, initialize some values
isCell = False
if isinstance(self.elem, Cell):
isCell = True
if 'columnsRepeated' in context:
nbOfColumns = sum(context['columnsRepeated'])
customColumnsRepeated = True
else:
nbOfColumns = self.elem.tableInfo.nbOfColumns
customColumnsRepeated = False
initialColIndex = self.elem.colIndex
currentColIndex = initialColIndex
rowAttributes = self.elem.tableInfo.curRowAttrs
# If p_elems is empty, dump an empty cell to avoid having the wrong
# number of cells for the current row.
if not elems:
result.dumpElement(Cell.OD.elem)
# Enter the "for" loop
loop, outerLoop = self.initialiseLoop(context, elems)
i = -1
for item in elems:
i += 1
loop.nb = i
loop.first = i == 0
loop.last = i == (loop.length-1)
loop.even = (i%2)==0
loop.odd = not loop.even
context[self.iter] = item
# Cell: add a new row if we are at the end of a row
if isCell and (currentColIndex == nbOfColumns):
result.dumpEndElement(Row.OD.elem)
result.dumpStartElement(Row.OD.elem, rowAttributes)
currentColIndex = 0
# If a sub-action is defined, execute it
if self.subAction:
self.subAction.execute(result, context)
else:
# Evaluate the buffer directly
self.evaluateBuffer(result, context)
# Cell: increment the current column index
if isCell:
currentColIndex += 1
# Cell: leave the last row with the correct number of cells, excepted
# if the user has specified himself "columnsRepeated": it is his
# responsibility to produce the correct number of cells.
if isCell and elems and not customColumnsRepeated:
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(result, context, 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(result, context, subElements=False)
result.dumpEndElement(Row.OD.elem)
# Create additional row with remaining cells
result.dumpStartElement(Row.OD.elem, rowAttributes)
nbOfRemainingCells = wrongNbOfCells + nbOfMissingCells
nbOfMissingCellsLastLine = nbOfColumns - nbOfRemainingCells
context[self.iter] = ''
for i in range(nbOfMissingCellsLastLine):
self.buffer.evaluate(result, context, subElements=False)
# Delete the current loop object and restore the overridden one if any
try:
delattr(context['loop'], self.iter)
except AttributeError:
pass
if outerLoop:
setattr(context['loop'], self.iter, outerLoop)
# Restore hidden variable if any
if hasHiddenVariable:
context[self.iter] = hiddenVariable
else:
if elems:
if self.iter in context: # May not be the case on error
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, result, context, exprRes):
self.evaluateBuffer(result, context)
class VariablesAction(BufferAction):
'''Action that allows to define a set of variables somewhere in the
template.'''
def __init__(self, name, buff, elem, minus, variables, src, fromExpr):
# We do not use the default Buffer.expr attribute for storing the Python
# expression, because here we will have several expressions, one for
# every defined variable.
BufferAction.__init__(self,name, buff, None, elem, minus, src, fromExpr)
# Definitions of variables: ~[(s_name, s_expr)]~
self.variables = variables
def do(self, result, context, exprRes):
'''Evaluate the variables' expressions: because there are several
expressions, we do not use the standard, single-expression-minded
BufferAction code for evaluating our expressions.
We remember the names and values of the variables that we will hide
in the context: after execution of this buffer we will restore those
values.
'''
hidden = None
for name, expr in self.variables:
# Evaluate variable expression in vRes
vRes, error = self.evaluateExpression(result, context, expr)
if error: return
# Replace the value of global variables
if name.startswith('@'):
context[name[1:]] = vRes
continue
# Remember the variable previous value if already in the context
if name in context:
if not hidden:
hidden = {name: context[name]}
else:
hidden[name] = context[name]
# Store the result into the context
context[name] = vRes
# If a sub-action is defined, execute it
if self.subAction:
self.subAction.execute(result, context)
else:
# Evaluate the buffer directly
self.evaluateBuffer(result, context)
# Restore hidden variables if any
if hidden: context.update(hidden)
# Delete not-hidden variables
for name, expr in self.variables:
if name.startswith('@'): continue
if hidden and (name in hidden): continue
del context[name]
# ------------------------------------------------------------------------------

733
appy/pod/buffers.py Normal file
View file

@ -0,0 +1,733 @@
# ------------------------------------------------------------------------------
# This file is part of Appy, a framework for building applications in the Python
# language. Copyright (C) 2007 Gaetan Delannay
# Appy is free software; you can redistribute it and/or modify it under the
# terms of the GNU General Public License as published by the Free Software
# Foundation; either version 3 of the License, or (at your option) any later
# version.
# Appy 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
# Appy. If not, see <http://www.gnu.org/licenses/>.
# ------------------------------------------------------------------------------
import re
from xml.sax.saxutils import quoteattr
from appy.shared.xml_parser import xmlPrologue, escapeXml
from appy.pod import PodError
from appy.shared.utils import Traceback
from appy.pod.elements import *
from appy.pod.actions import IfAction, ElseAction, ForAction, VariablesAction, \
NullAction
# ------------------------------------------------------------------------------
class ParsingError(Exception): pass
class EvaluationError(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
BAD_VAR_EXPRESSION = 'Bad variable definition "%s". A variable definition ' \
'must have the form {name} = {expression}. {name} must be a Python-' \
'compliant variable name. {expression} is a Python expression. When ' \
'encountering such a statement, pod will define, in the specified part ' \
'of the document, a variable {name} whose value will be the evaluated ' \
'{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 = list(self.buffer.subBuffers.keys())
self.remainingElemIndexes = list(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
# Are we computing for pod (True) or px (False)
self.pod = env.__class__.__name__ != 'PxEnvironment'
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 = list(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 patchTableElement(self, elem, attrs):
'''Convert the name of a table to an expression allowing the user to
define himself this name via variable "tableName".
Convert attribute "number-columns-repeated" of every table column
(or add it if it does not exist) to let the user define how he will
repeat table columns via variable "columnsRepeated".'''
if elem == self.env.tags['table']:
attrs = attrs._attrs
name = self.env.tags['table-name']
attrs[name] = ':tableName|"%s"' % attrs[name]
elif elem == self.env.tags['table-column']:
attrs = attrs._attrs
key = self.env.tags['number-columns-repeated']
columnNumber = self.env.getTable().nbOfColumns -1
nb = (key in attrs) and attrs[key] or '1'
attrs[key] = ':columnsRepeated[%d]|%s' % (columnNumber, nb)
def dumpStartElement(self, elem, attrs={}, ignoreAttrs=(), hook=False,
noEndTag=False, renamedAttrs=None):
'''Inserts into this buffer the start tag p_elem, with its p_attrs,
excepted those listed in p_ignoreAttrs. Attrs can be dumped with an
alternative name if specified in dict p_renamedAttrs. If p_hook is
not None (works only for MemoryBuffers), we will insert, at the end
of the list of dumped attributes:
* [pod] an Attributes instance, in order to be able, when evaluating
the buffer, to dump additional attributes, not known at this
dump time;
* [px] an Attribute instance, representing a special HTML attribute
like "checked" or "selected", that, if the tied expression
returns False, must not be dumped at all. In this case,
p_hook must be a tuple (s_attrName, s_expr).
'''
self.write('<%s' % elem)
# Some table elements must be patched (pod only)
if self.pod: self.patchTableElement(elem, attrs)
for name, value in list(attrs.items()):
if ignoreAttrs and (name in ignoreAttrs): continue
if renamedAttrs and (name in renamedAttrs): name=renamedAttrs[name]
# If the value begins with ':', it is a Python expression. Else,
# it is a static value.
if not value.startswith(':'):
self.write(' %s=%s' % (name, quoteattr(value)))
else:
self.write(' %s="' % name)
self.addExpression(value[1:])
self.write('"')
res = None
if hook:
if self.pod:
res = self.addAttributes()
else:
self.addAttribute(*hook)
# Close the tag
self.write(noEndTag and '/>' or '>')
return res
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.'''
if self.pod:
# Take care of converting line breaks and tabs
content = escapeXml(content, format='odf',
nsText=self.env.namespaces[self.env.NS_TEXT])
else:
content = escapeXml(content)
self.write(content)
# ------------------------------------------------------------------------------
class FileBuffer(Buffer):
def __init__(self, env, result):
Buffer.__init__(self, env, None)
self.result = result
self.content = file(result, 'w')
self.content.write(xmlPrologue)
# 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 getLength(self): return 0
def write(self, something):
try:
self.content.write(something.encode('utf-8'))
except UnicodeDecodeError:
self.content.write(something)
def addExpression(self, expression, tiedHook=None):
# At 2013-02-06, this method was not called within the whole test suite.
try:
expr = Expression(expression, self.pod)
if tiedHook: tiedHook.tiedExpression = expr
res, escape = expr.evaluate(self.env.context)
if escape: self.dumpContent(res)
else: self.write(res)
except Exception as e:
if not self.env.raiseOnError:
PodError.dump(self, EVAL_EXPR_ERROR % (expression, e),
dumpTb=False)
else:
raise Exception(EVAL_EXPR_ERROR % (expression, e))
def addAttributes(self):
# Into a FileBuffer, it is not possible to insert Attributes. Every
# Attributes instance is tied to an Expression; because dumping
# expressions directly into FileBuffer instances seems to be a rather
# theorical case (see comment inside the previous method), it does not
# seem to be a real problem.
pass
def pushSubBuffer(self, subBuffer): pass
def getRootBuffer(self): return self
# ------------------------------------------------------------------------------
class MemoryBuffer(Buffer):
actionRex = re.compile('(?:(\w+)\s*\:\s*)?do\s+(\w+)(-)?' \
'(?:\s+(for|if|else|with)\s*(.*))?')
forRex = re.compile('\s*([\w\-_]+)\s+in\s+(.*)')
varRex = re.compile('\s*(@?[\w\-_]+)\s*=\s*(.*)')
def __init__(self, env, parent):
Buffer.__init__(self, env, parent)
self.content = ''
self.elements = {}
self.action = None
def clone(self):
'''Produces an empty buffer that is a clone of this one.'''
return MemoryBuffer(self.env, self.parent)
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 getRootBuffer(self):
'''Returns the root buffer. For POD it is always a FileBuffer. For PX,
it is a MemoryBuffer.'''
if self.parent: return self.parent.getRootBuffer()
return self
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.items():
if podElem.__class__.__name__.lower() == podElemName:
if index > res:
res = index
return res
def getMainElement(self):
res = None
if 0 in self.elements:
res = self.elements[0]
return res
def isMainElement(self, elem):
'''Is p_elem the main element within this buffer?'''
mainElem = self.getMainElement()
if not mainElem: return
if hasattr(mainElem, 'OD'): mainElem = mainElem.OD.elem
if elem != mainElem: return
# elem is the same as the main elem. But is it really the main elem, or
# the same elem, found deeper in the buffer?
for index, iElem in self.elements.items():
foundElem = None
if hasattr(iElem, 'OD'):
if iElem.OD:
foundElem = iElem.OD.elem
else:
foundElem = iElem
if (foundElem == mainElem) and (index != 0):
return
return True
def unreferenceElement(self, elem):
# Find last occurrence of this element
elemIndex = -1
for index, iElem in self.elements.items():
foundElem = None
if hasattr(iElem, 'OD'):
# A POD element
if iElem.OD:
foundElem = iElem.OD.elem
else:
# A PX elem
foundElem = iElem
if (foundElem == 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.items():
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 += ' '
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(self.parent, self.env.context)
else:
# Transfer content in itself
oldParentLength = self.parent.getLength()
self.parent.write(self.content)
# Transfer elements
for index, podElem in self.elements.items():
self.parent.elements[oldParentLength + index] = podElem
# Transfer sub-buffers
for index, buf in self.subBuffers.items():
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, elemType='pod'):
if elemType == 'pod':
elem = PodElement.create(elem)
self.elements[self.getLength()] = elem
if isinstance(elem, Cell) or isinstance(elem, Table):
elem.tableInfo = self.env.getTable()
if isinstance(elem, Cell):
# Remember where this cell is in the table
elem.colIndex = elem.tableInfo.curColIndex
if elem == 'x':
# See comment on similar statement in the method below.
self.content += ' '
def addExpression(self, expression, tiedHook=None):
# Create the POD expression
expr = Expression(expression, self.pod)
if tiedHook: tiedHook.tiedExpression = expr
self.elements[self.getLength()] = expr
# To be sure that an expr and an elem can't be found at the same index
# in the buffer.
self.content += ' '
def addAttributes(self):
'''pod-only: adds an Attributes instance into this buffer.'''
attrs = Attributes(self.env)
self.elements[self.getLength()] = attrs
self.content += ' '
return attrs
def addAttribute(self, name, expr):
'''px-only: adds an Attribute instance into this buffer.'''
attr = Attribute(name, expr)
self.elements[self.getLength()] = attr
self.content += ' '
return attr
def _getVariables(self, expr):
'''Returns variable definitions in p_expr as a list
~[(s_varName, s_expr)]~.'''
exprs = expr.strip().split(';')
res = []
for sub in exprs:
varRes = MemoryBuffer.varRex.match(sub)
if not varRes:
raise ParsingError(BAD_VAR_EXPRESSION % sub)
res.append(varRes.groups())
return res
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 list(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.action.name in self.env.namedIfActions:
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 ifReference not in self.env.namedIfActions:
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)
elif actionType == 'with':
variables = self._getVariables(subExpr)
self.action = VariablesAction(statementName, self, podElem,
minus, variables, 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 as ppe:
PodError.dump(self, ppe, removeFirstLine=True)
return res
def createPxAction(self, elem, actionType, statement):
'''Creates a PX action and link it to this buffer. If an action is
already linked to this buffer (in self.action), this action is
chained behind the last action via self.action.subAction.'''
res = 0
statement = statement.strip()
if actionType == 'for':
forRes = MemoryBuffer.forRex.match(statement)
if not forRes:
raise ParsingError(BAD_FOR_EXPRESSION % statement)
iter, subExpr = forRes.groups()
action = ForAction('for', self, subExpr, elem, False, iter,
'buffer', None)
elif actionType == 'if':
action= IfAction('if', self, statement, elem, False, 'buffer', None)
elif actionType in ('var', 'var2'):
variables = self._getVariables(statement)
action = VariablesAction('var', self, elem, False, variables,
'buffer', None)
# Is it the first action for this buffer or not?
if not self.action:
self.action = action
else:
self.action.addSubAction(action)
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 = next(iter)
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.items():
elements[elemIndex-index] = elem
self.elements = elements
subBuffers = {}
for subIndex, buf in self.subBuffers.items():
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.items():
condition = isinstance(elem, Expression) or \
isinstance(elem, Attributes)
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 not removeMainElems: return 0
# 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
return pos + 1
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 removeAutomaticExpressions(self):
'''When a buffer has an action with minus=True, we must remove the
"columnsRepeat" expressions automatically inserted by pod. Else, we
will have problems when computing the index of the part to keep
(m_getStartIndex).'''
# Find the start position of the deepest element to remove
deepestElem = self.action.elem.DEEPEST_TO_REMOVE
pos = self.content.find('<%s' % deepestElem.elem)
for index in self.elements.keys():
if index < pos: del self.elements[index]
reTagContent = re.compile('<(?P<p>[\w-]+):(?P<f>[\w-]+)(.*?)>.*</(?P=p):' \
'(?P=f)>', re.S)
def evaluate(self, result, context, subElements=True,
removeMainElems=False):
'''Evaluates this buffer given the current p_context and add the result
into p_result. With pod, p_result is the root file buffer; with px
it is a memory buffer.'''
if not subElements:
# Dump the root tag in this buffer, but not its content
res = self.reTagContent.match(self.content.strip())
if not res: result.write(self.content)
else:
g = res.group
result.write('<%s:%s%s></%s:%s>' % (g(1),g(2),g(3),g(1),g(2)))
else:
if removeMainElems: self.removeAutomaticExpressions()
iter = BufferIterator(self)
currentIndex = self.getStartIndex(removeMainElems)
while iter.hasNext():
index, evalEntry = next(iter)
result.write(self.content[currentIndex:index])
currentIndex = index + 1
if isinstance(evalEntry, Expression):
try:
res, escape = evalEntry.evaluate(context)
if escape: result.dumpContent(res)
else: result.write(res)
except EvaluationError, e:
# This exception has already been treated (see the
# "except" block below). Simply re-raise it when needed.
if self.env.raiseOnError: raise e
except Exception as e:
if not self.env.raiseOnError:
PodError.dump(result, EVAL_EXPR_ERROR % (
evalEntry.expr, e))
else:
raise EvaluationError(EVAL_EXPR_ERROR % \
(evalEntry.expr, '\n'+Traceback.get(5)))
elif isinstance(evalEntry, Attributes) or \
isinstance(evalEntry, Attribute):
result.write(evalEntry.evaluate(context))
else: # It is a subBuffer
if evalEntry.action:
evalEntry.action.execute(result, context)
else:
result.write(evalEntry.content)
stopIndex = self.getStopIndex(removeMainElems)
if currentIndex < (stopIndex-1):
result.write(self.content[currentIndex:stopIndex])
def clean(self):
'''Cleans the buffer content.'''
self.content = ''
# ------------------------------------------------------------------------------

308
appy/pod/converter.py Normal file
View file

@ -0,0 +1,308 @@
# ------------------------------------------------------------------------------
# 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
htmlFilters = {'odt': 'HTML (StarWriter)',
'ods': 'HTML (StarCalc)',
'odp': 'impress_html_Export'}
FILE_TYPES = {'odt': 'writer8',
'ods': 'calc8',
'odp': 'impress8',
'htm': htmlFilters, 'html': htmlFilters,
'rtf': 'Rich Text Format',
'txt': 'Text',
'csv': 'Text - txt - csv (StarCalc)',
'pdf': {'odt': 'writer_pdf_Export', 'ods': 'calc_pdf_Export',
'odp': 'impress_pdf_Export', 'htm': 'writer_pdf_Export',
'html': 'writer_pdf_Export', 'rtf': 'writer_pdf_Export',
'txt': 'writer_pdf_Export', 'csv': 'calc_pdf_Export',
'swf': 'draw_pdf_Export', 'doc': 'writer_pdf_Export',
'xls': 'calc_pdf_Export', 'ppt': 'impress_pdf_Export',
'docx': 'writer_pdf_Export', 'xlsx': 'calc_pdf_Export'
},
'swf': 'impress_flash_Export',
'doc': 'MS Word 97',
'xls': 'MS Excel 97',
'ppt': 'MS PowerPoint 97',
'docx': 'MS Word 2007 XML',
'xlsx': 'Calc MS Excel 2007 XML',
}
# Conversion from odt to odt does not make any conversion, but updates indexes
# and linked documents.
# ------------------------------------------------------------------------------
class ConverterError(Exception): pass
# ConverterError-related messages ----------------------------------------------
DOC_NOT_FOUND = '"%s" 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 LibreOffice on port %d. UNO ' \
'(LibreOffice API) says: %s.'
# Some constants ---------------------------------------------------------------
DEFAULT_PORT = 2002
# ------------------------------------------------------------------------------
class Converter:
'''Converts a document readable by LibreOffice into pdf, doc, txt, 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,
templatePath=None):
self.port = port
# The path to the document to convert
self.docUrl, self.docPath = self.getFilePath(docPath)
self.inputType = os.path.splitext(docPath)[1][1:].lower()
self.resultType = resultType
self.resultFilter = self.getResultFilter()
self.resultUrl = self.getResultUrl()
self.loContext = None
self.oo = None # The LibreOffice application object
self.doc = None # The LibreOffice loaded document
# The path to a LibreOffice template (ie, a ".ott" file) from which
# styles can be imported
self.templateUrl = self.templatePath = None
if templatePath:
self.templateUrl, self.templatePath = self.getFilePath(templatePath)
def getFilePath(self, filePath):
'''Returns the absolute path of p_filePath. In fact, it returns a
tuple with some URL version of the path for LO as the first element
and the absolute path as the second element.'''
import unohelper
if not os.path.exists(filePath) and not os.path.isfile(filePath):
raise ConverterError(DOC_NOT_FOUND % filePath)
docAbsPath = os.path.abspath(filePath)
# Return one path for OO, one path for me
return unohelper.systemPathToFileUrl(docAbsPath), docAbsPath
def getResultFilter(self):
'''Based on the result type, identifies which OO filter to use for the
document conversion.'''
if self.resultType in FILE_TYPES:
res = FILE_TYPES[self.resultType]
if isinstance(res, dict):
res = res[self.inputType]
else:
raise ConverterError(BAD_RESULT_TYPE % (self.resultType,
list(FILE_TYPES.keys())))
return res
def getResultUrl(self):
'''Returns the path of the result file in the format needed by LO. If
the result type and the input type are the same (ie the user wants to
refresh indexes or some other action and not perform a real
conversion), the result file is named
<inputFileName>.res.<resultType>.
Else, the result file is named like the input file but with a
different extension:
<inputFileName>.<resultType>
'''
import unohelper
baseName = os.path.splitext(self.docPath)[0]
if self.resultType != self.inputType:
res = '%s.%s' % (baseName, self.resultType)
else:
res = '%s.res.%s' % (baseName, self.resultType)
try:
f = open(res, 'w')
f.write('Hello')
f.close()
os.remove(res)
return unohelper.systemPathToFileUrl(res)
except (OSError, IOError):
e = sys.exc_info()[1]
raise ConverterError(CANNOT_WRITE_RESULT % (res, e))
def props(self, properties):
'''Create a UNO-compliant tuple of properties, from tuple p_properties
containing sub-tuples (s_propertyName, value).'''
from com.sun.star.beans import PropertyValue
res = []
for name, value in properties:
prop = PropertyValue()
prop.Name = name
prop.Value = value
res.append(prop)
return tuple(res)
def connect(self):
'''Connects to LibreOffice'''
if os.name == 'nt':
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.loContext = 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 LibreOffice instance), this method
# blocks.
smgr = self.loContext.ServiceManager
# Get the central desktop object
self.oo = smgr.createInstanceWithContext(
'com.sun.star.frame.Desktop', self.loContext)
except NoConnectException:
e = sys.exc_info()[1]
raise ConverterError(CONNECT_ERROR % (self.port, e))
def updateOdtDocument(self):
'''If the input file is an ODT document, we will perform those tasks:
1) update all annexes;
2) update sections (if sections refer to external content, we try to
include the content within the result file);
3) load styles from an external template if given.
'''
from com.sun.star.lang import 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)
# 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
# Import styles from an external file when required
if self.templateUrl:
params = self.props(('OverwriteStyles', True),
('LoadPageStyles', False))
self.doc.StyleFamilies.loadStylesFromURL(self.templateUrl, params)
def loadDocument(self):
from com.sun.star.lang import IllegalArgumentException, \
IndexOutOfBoundsException
try:
# Loads the document to convert in a new hidden frame
props = [('Hidden', True)]
if self.inputType == 'csv':
# Give some additional params if we need to open a CSV file
props.append(('FilterFlags', '59,34,76,1'))
#props.append(('FilterData', 'Any'))
self.doc = self.oo.loadComponentFromURL(self.docUrl, "_blank", 0,
self.props(props))
# Perform additional tasks for odt documents
if self.inputType == 'odt': self.updateOdtDocument()
try:
self.doc.refresh()
except AttributeError:
pass
except IllegalArgumentException:
e = sys.exc_info()[1]
raise ConverterError(URL_NOT_FOUND % (self.docPath, e))
def convertDocument(self):
'''Calls LO to perform a document conversion. Note that the conversion
is not really done if the source and target documents have the same
type.'''
props = [('FilterName', self.resultFilter)]
if self.resultType == 'csv': # Add options for CSV export (separator...)
props.append(('FilterOptions', '59,34,76,1'))
self.doc.storeToURL(self.resultUrl, self.props(props))
def run(self):
'''Connects to LO, does the job and disconnects'''
self.connect()
self.loadDocument()
self.convertDocument()
self.doc.close(True)
# 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 file you want to convert (or whose content like\n' \
' indexes need to be refreshed);\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 which is included in the LibreOffice distribution).' % \
str(list(FILE_TYPES.keys()))
def run(self):
optParser = OptionParser(usage=ConverterScript.usage)
optParser.add_option("-p", "--port", dest="port",
help="The port on which LibreOffice runs " \
"Default is %d." % DEFAULT_PORT,
default=DEFAULT_PORT, metavar="PORT", type='int')
optParser.add_option("-t", "--template", dest="template",
default=None, metavar="TEMPLATE", type='string',
help="The path to a LibreOffice template from " \
"which you may import styles.")
(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, options.template)
try:
converter.run()
except ConverterError:
e = sys.exc_info()[1]
sys.stderr.write(str(e))
sys.stderr.write('\n')
optParser.print_help()
sys.exit(ERROR_CODE)
# ------------------------------------------------------------------------------
if __name__ == '__main__':
ConverterScript().run()
# ------------------------------------------------------------------------------

423
appy/pod/doc_importers.py Normal file
View file

@ -0,0 +1,423 @@
# ------------------------------------------------------------------------------
# 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, random, urllib.parse
from appy.pod import PodError
from appy.pod.odf_parser import OdfEnvironment
from appy.shared import mimeTypesExts
from appy.shared.utils import FileWrapper
from appy.shared.dav import Resource
# The uuid module is there only if python >= 2.5
try:
import uuid
except ImportError:
uuid = None
# ------------------------------------------------------------------------------
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.'
CONVERT_ERROR = 'Program "convert", from imagemagick, must be installed and ' \
'in the path for converting a SVG file into a PNG file. ' \
'Conversion of SVG files must also be enabled. On Ubuntu: ' \
'apt-get install librsvg2-bin'
TO_PDF_ERROR = 'ConvertImporter error while converting a doc to PDF: %s.'
# ------------------------------------------------------------------------------
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, renderer):
self.content = content
# If content is None, p_at tells us where to find it (file system path,
# url, etc)
self.at = at
# Ensure this path exists, if it is a local path.
if at and not at.startswith('http') and not os.path.isfile(at):
raise PodError(FILE_NOT_FOUND % at)
self.format = format
self.res = ''
self.renderer = renderer
self.ns = renderer.currentParser.env.namespaces
# Unpack some useful namespaces
self.textNs = self.ns[OdfEnvironment.NS_TEXT]
self.linkNs = self.ns[OdfEnvironment.NS_XLINK]
self.drawNs = self.ns[OdfEnvironment.NS_DRAW]
self.svgNs = self.ns[OdfEnvironment.NS_SVG]
self.tempFolder = renderer.tempFolder
self.importFolder = self.getImportFolder()
# Create the import folder if it does not exist.
if not os.path.exists(self.importFolder): os.mkdir(self.importFolder)
self.importPath = self.getImportPath(at, format)
# A link to the global fileNames dict (explained in renderer.py)
self.fileNames = renderer.fileNames
if at:
# Move the file within the ODT, if it is an image and if this image
# has not already been imported.
self.importPath = self.moveFile(at, self.importPath)
else:
# We need to dump the file content (in self.content) in a temp file
# first. self.content may be binary, a file handler or a
# FileWrapper.
if isinstance(self.content, FileWrapper):
self.content.dump(self.importPath)
else:
if isinstance(self.content, file):
fileContent = self.content.read()
else:
fileContent = self.content
f = file(self.importPath, 'wb')
f.write(fileContent)
f.close()
# Some importers add specific attrs, through method init.
def getUuid(self):
'''Creates a unique id for images/documents to be imported into an
ODT document.'''
if uuid:
return uuid.uuid4().hex
else:
# The uuid module is not there. Generate a UUID based on random.
return 'f%d.%f' % (random.randint(0,1000), time.time())
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.'''
def getImportPath(self, at, format):
'''Gets the path name of the file to dump on disk (within the ODT for
images, in a temp folder for docs).'''
if not format:
if at.startswith('http'):
format = '' # We will know it only after the HTTP GET.
else:
format = os.path.splitext(at)[1][1:]
fileName = '%s.%s' % (self.getUuid(), format)
return os.path.abspath('%s/%s' % (self.importFolder, fileName))
def moveFile(self, at, importPath):
'''In the case parameter "at" was used, we may want to move the file at
p_at within the ODT result in p_importPath (for images) or do
nothing (for docs). In the latter case, the file to import stays
at _at, and is not copied into p_importPath. So the previously
computed p_importPath is not used at all.'''
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 init(self, pageBreakBefore, pageBreakAfter):
'''OdtImporter-specific constructor.'''
self.pageBreakBefore = pageBreakBefore
self.pageBreakAfter = pageBreakAfter
def run(self):
# Define a "pageBreak" if needed.
if self.pageBreakBefore or self.pageBreakAfter:
pageBreak = '<%s:p %s:style-name="podPageBreak"></%s:p>' % \
(self.textNs, self.textNs, self.textNs)
# Insert a page break before importing the doc if needed
if self.pageBreakBefore: self.res += pageBreak
# Import the external odt document
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)
# Insert a page break after importing the doc if needed
if self.pageBreakAfter: self.res += pageBreak
return self.res
class PodImporter(DocImporter):
'''This class allows to import the result of applying another POD template,
into the current POD result.'''
def getImportFolder(self): return '%s/docImports' % self.tempFolder
def init(self, context, pageBreakBefore, pageBreakAfter):
'''PodImporter-specific constructor.'''
self.context = context
self.pageBreakBefore = pageBreakBefore
self.pageBreakAfter = pageBreakAfter
def run(self):
# Define where to store the pod result in the temp folder
r = self.renderer
# Define where to store the ODT result.
op = os.path
resOdt = op.join(self.getImportFolder(), '%s.odt' % self.getUuid())
# The POD template is in self.importPath
renderer = r.__class__(self.importPath, self.context, resOdt,
pythonWithUnoPath=r.pyPath,
ooPort=r.ooPort, forceOoCall=r.forceOoCall,
imageResolver=r.imageResolver)
renderer.stylesManager.stylesMapping = r.stylesManager.stylesMapping
renderer.run()
# The POD result is in "resOdt". Import it into the main POD result
# using an OdtImporter.
odtImporter = OdtImporter(None, resOdt, 'odt', self.renderer)
odtImporter.init(self.pageBreakBefore, self.pageBreakAfter)
return odtImporter.run()
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.'''
def getImportFolder(self): return '%s/docImports' % self.tempFolder
def run(self):
imagePrefix = os.path.splitext(os.path.basename(self.importPath))[0]
# 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, imagePrefix, self.importPath)
os.system(cmd)
# Check that at least one image was generated
succeeded = False
firstImage = '%s1.jpg' % 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, imagePrefix, i)
if os.path.exists(nextImage):
# Use internally an Image importer for doing this job.
imgImporter= ImageImporter(None, nextImage, 'jpg',self.renderer)
imgImporter.init('paragraph', True, None, None, None)
self.res += imgImporter.run()
os.remove(nextImage)
else:
noMoreImages = True
return self.res
class ConvertImporter(DocImporter):
'''This class allows to import the content of any file that LibreOffice (LO)
can convert into PDF: doc, rtf, xls. It first calls LO to convert the
document into PDF, then calls a PdfImporter.'''
def getImportFolder(self): return '%s/docImports' % self.tempFolder
def run(self):
# Convert the document into PDF with LibreOffice
output = self.renderer.callLibreOffice(self.importPath, 'pdf')
if output: raise PodError(TO_PDF_ERROR % output)
pdfFile = '%s.pdf' % os.path.splitext(self.importPath)[0]
# Launch a PdfImporter to import this PDF into the POD result.
pdfImporter = PdfImporter(None, pdfFile, 'pdf', self.renderer)
return pdfImporter.run()
# 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)
# Get fileType from filePath if not given.
if not fileType: fileType = os.path.splitext(filePath)[1][1:]
f = file(filePath, 'rb')
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)
f.close()
if x and y:
return float(x)/pxToCm, float(y)/pxToCm
else:
return x, y
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.'
pictFolder = '%sPictures%s' % (os.sep, os.sep)
def getImportFolder(self):
return os.path.join(self.tempFolder, 'unzip', 'Pictures')
def moveFile(self, at, importPath):
'''Copies file at p_at into the ODT file at p_importPath.'''
# Has this image already been imported ?
for imagePath, imageAt in self.fileNames.items():
if imageAt == at:
# Yes!
i = importPath.rfind(self.pictFolder) + 1
return importPath[:i] + imagePath
# The image has not already been imported: copy it.
if not at.startswith('http'):
shutil.copy(at, importPath)
return importPath
# The image must be retrieved via a URL. Try to perform a HTTP GET.
response = Resource(at).get()
if response.code == 200:
# At last, I can get the file format.
self.format = mimeTypesExts[response.headers['Content-Type']]
importPath += self.format
f = file(importPath, 'wb')
f.write(response.body)
f.close()
return importPath
# The HTTP GET did not work, maybe for security reasons (we probably
# have no permission to get the file). But maybe the URL was a local
# one, from an application server running this POD code. In this case,
# if an image resolver has been given to POD, use it to retrieve the
# image.
imageResolver = self.renderer.imageResolver
if not imageResolver:
# Return some default image explaining that the image wasn't found.
import appy.pod
podFolder = os.path.dirname(appy.pod.__file__)
img = os.path.join(podFolder, 'imageNotFound.jpg')
self.format = 'jpg'
importPath += self.format
f = file(img)
imageContent = f.read()
f.close()
f = file(importPath, 'wb')
f.write(imageContent)
f.close()
else:
# The imageResolver is a Zope application. From it, we will
# retrieve the object on which the image is stored and get
# the file to download.
urlParts = urllib.parse.urlsplit(at)
path = urlParts[2][1:].split('/')[:-1]
try:
obj = imageResolver.unrestrictedTraverse(path)
except KeyError:
# Maybe a rewrite rule as added some prefix to all URLs?
obj = imageResolver.unrestrictedTraverse(path[1:])
zopeFile = getattr(obj, urlParts[3].split('=')[1])
appyFile = FileWrapper(zopeFile)
self.format = mimeTypesExts[appyFile.mimeType]
importPath += self.format
appyFile.dump(importPath)
return importPath
def init(self, anchor, wrapInPara, size, sizeUnit, style):
'''ImageImporter-specific constructor.'''
# Initialise anchor
if anchor not in self.anchorTypes:
raise PodError(self.WRONG_ANCHOR % str(self.anchorTypes))
self.anchor = anchor
self.wrapInPara = wrapInPara
self.size = size
self.sizeUnit = sizeUnit
# Put CSS attributes from p_style in a dict.
self.cssAttrs = {}
if style:
for attr in style.split(';'):
if not attr.strip(): continue
name, value = attr.strip().split(':')
value = value.strip()
if value.endswith('px'): value = value[:-2]
if value.isdigit(): value=int(value)
self.cssAttrs[name.strip()] = value
def run(self):
# Some shorcuts for the used xml namespaces
d = self.drawNs
t = self.textNs
x = self.linkNs
s = self.svgNs
# Compute path to image
i = self.importPath.rfind(self.pictFolder)
imagePath = self.importPath[i+1:].replace('\\', '/')
self.fileNames[imagePath] = self.at
# In the case of SVG files, perform an image conversion to PNG
if imagePath.endswith('.svg'):
newImportPath = os.path.splitext(self.importPath)[0] + '.png'
err= os.system('convert "%s" "%s"'% (self.importPath,newImportPath))
if err:
raise Exception(CONVERT_ERROR)
os.remove(self.importPath)
self.importPath = newImportPath
imagePath = os.path.splitext(imagePath)[0] + '.png'
self.format = 'png'
# Retrieve image size from self.size
width = height = None
if self.size and (self.sizeUnit != 'pc'):
width, height = self.size
if self.sizeUnit == 'px':
# Convert it to cm
width = float(width) / pxToCm
height = float(height) / pxToCm
# Override self.size if 'height' or 'width' is found in self.cssAttrs
if 'width' in self.cssAttrs:
width = float(self.cssAttrs['width']) / pxToCm
if 'height' in self.cssAttrs:
height = float(self.cssAttrs['height']) / pxToCm
# If width and/or height is missing, compute it.
if not width or not height:
width, height = getSize(self.importPath, self.format)
if self.sizeUnit == 'pc':
# Apply the given percentage to the real width and height.
width = width * (float(self.size[0])/100)
height = height * (float(self.size[1])/100)
if width != None:
size = ' %s:width="%fcm" %s:height="%fcm"' % (s, width, s, height)
else:
size = ''
if 'float' in self.cssAttrs:
floatValue = self.cssAttrs['float'].capitalize()
styleInfo = '%s:style-name="podImage%s" ' % (d, floatValue)
self.anchor = 'char'
else:
styleInfo = ''
image = '<%s:frame %s%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>' % (d, styleInfo, d, self.getUuid(), d, t,
self.anchor, size, d, x, x, x, imagePath, x, d)
if hasattr(self, 'wrapInPara') and self.wrapInPara:
image = '<%s:p>%s</%s:p>' % (t, image, t)
self.res += image
return self.res
# ------------------------------------------------------------------------------

230
appy/pod/elements.py Normal file
View file

@ -0,0 +1,230 @@
# ------------------------------------------------------------------------------
# This file is part of Appy, a framework for building applications in the Python
# language. Copyright (C) 2007 Gaetan Delannay
# Appy is free software; you can redistribute it and/or modify it under the
# terms of the GNU General Public License as published by the Free Software
# Foundation; either version 3 of the License, or (at your option) any later
# version.
# Appy 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
# Appy. If not, see <http://www.gnu.org/licenses/>.
# ------------------------------------------------------------------------------
from xml.sax.saxutils import quoteattr
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')
# Elements for which the '-' operator can be applied.
MINUS_ELEMS = ('section', 'table')
@staticmethod
def create(elem):
'''Used to create any POD elem that has an equivalent OD element. Not
for creating expressions, for example.'''
return eval(PodElement.OD_TO_POD[elem])()
class Text(PodElement):
OD = XmlElement('p', nsUri=ns.NS_TEXT)
# When generating an error we may need to surround it with a given tag and
# sub-tags.
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]
# When we must remove the Section element from a buffer, the deepest element
# to remove is the Section element itself.
DEEPEST_TO_REMOVE = OD
class Cell(PodElement):
OD = XmlElement('table-cell', nsUri=ns.NS_TABLE)
subTags = [Text.OD]
def __init__(self):
self.tableInfo = None # ~OdTable~
self.colIndex = None # The column index for this cell, within its table.
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]
# 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).
DEEPEST_TO_REMOVE = Cell.OD
def __init__(self):
self.tableInfo = None # ~OdTable~
class Expression(PodElement):
'''Represents a Python expression that is found in a pod or px.'''
OD = None
def extractInfo(self, py):
'''Within p_py, several elements can be included:
- the fact that XML chars must be escaped or not (leading ":")
- the "normal" Python expression,
- an optional "error" expression, that is evaluated when the normal
expression raises an exception.
This method return a tuple (escapeXml, normaExpr, errorExpr).'''
# Determine if we must escape XML chars or not.
escapeXml = True
if py.startswith(':'):
py = py[1:]
escapeXml = False
# Extract normal and error expression
if '|' not in py:
expr = py
errorExpr = None
else:
expr, errorExpr = py.rsplit('|', 1)
expr = expr.strip()
errorExpr = errorExpr.strip()
return escapeXml, expr, errorExpr
def __init__(self, py, pod):
# Extract parts from expression p_py.
self.escapeXml, self.expr, self.errorExpr = self.extractInfo(py.strip())
self.pod = pod # True if I work for pod, False if I work for px.
if self.pod:
# pod-only: store here the expression's true result (before being
# converted to a string).
self.result = None
# pod-only: the following bool indicates if this Expression instance
# has already been evaluated or not. Expressions which are tied to
# attribute hooks are already evaluated when the tied hook is
# evaluated: this boolean prevents the expression from being
# evaluated twice.
self.evaluated = False
# self.result and self.evaluated are not used by PX, because they
# are not thread-safe.
def _eval(self, context):
'''Evaluates self.expr with p_context. If self.errorExpr is defined,
evaluate it if self.expr raises an error.'''
if self.errorExpr:
try:
res = eval(self.expr, context)
except Exception:
res = eval(self.errorExpr, context)
else:
res = eval(self.expr, context)
return res
def evaluate(self, context):
'''Evaluates the Python expression (self.expr) with a given
p_context, and returns the result. More precisely, it returns a
tuple (result, escapeXml). Boolean escapeXml indicates if XML chars
must be escaped or not.'''
escapeXml = self.escapeXml
# Evaluate the expression, or get it from self.result if it has already
# been computed.
if self.pod and self.evaluated:
res = self.result
# It can happen only once, to ask to evaluate an expression that
# was already evaluated (from the tied hook). We reset here the
# boolean "evaluated" to allow for the next evaluation, probably
# with another context.
self.evaluated = False
else:
res = self._eval(context)
# pod-only: cache the expression result.
if self.pod: self.result = res
# Converts the expr result to a string that can be inserted in the
# pod/px result.
resultType = res.__class__.__name__
if resultType == 'NoneType':
res = ''
elif resultType == 'str':
res = res.decode('utf-8')
elif resultType == 'unicode':
pass # Don't perform any conversion, unicode is the target type.
elif resultType == 'Px':
# A PX that must be called within the current PX. Call it with the
# current context.
res = res(context, applyTemplate=False)
# Force escapeXml to False.
escapeXml = False
else:
res = str(res)
return res, escapeXml
class Attributes(PodElement):
'''Represents a bunch of XML attributes that will be dumped for a given tag
in the result. pod-only.'''
OD = None
floatTypes = ('int', 'long', 'float')
dateTypes = ('DateTime',)
def __init__(self, env):
self.attrs = {}
# Depending on the result of a tied expression, we will dump, for
# another tag, the series of attrs that this instance represents.
self.tiedExpression = None
# We will need the env to get the full names of attributes to dump.
self.env = env
def computeAttributes(self, expr):
'''p_expr has been evaluated: its result is in expr.result. Depending
on its type, we will dump the corresponding attributes in
self.attrs.'''
exprType = expr.result.__class__.__name__
tags = self.env.tags
attrs = self.attrs
if exprType in self.floatTypes:
attrs[tags['value-type']] = 'float'
attrs[tags['value']] = str(expr.result)
elif exprType in self.dateTypes:
attrs[tags['value-type']] = 'date'
attrs[tags['value']] = expr.result.strftime('%Y-%m-%d')
else:
attrs[tags['value-type']] = 'string'
def evaluate(self, context):
# Evaluate first the tied expression, in order to determine its type.
try:
self.tiedExpression.evaluate(context)
self.tiedExpression.evaluated = True
except Exception as e:
# Don't set "evaluated" to True. This way, when the buffer will
# evaluate the expression directly, we will really evaluate it, so
# the error will be dumped into the pod result.
pass
# Analyse the return type of the expression.
self.computeAttributes(self.tiedExpression)
# Now, self.attrs has been populated. Transform it into a string.
res = ''
for name, value in self.attrs.items():
res += ' %s=%s' % (name, quoteattr(value))
return res
class Attribute(PodElement):
'''Represents an HTML special attribute like "selected" or "checked".
px-only.'''
OD = None
def __init__(self, name, expr):
# The name of the attribute
self.name = name
# The expression that will compute the attribute value
self.expr = expr.strip()
def evaluate(self, context):
# If the expr evaluates to False, we do not dump the attribute at all.
if eval(self.expr, context): return ' %s="%s"' % (self.name, self.name)
return ''
# ------------------------------------------------------------------------------

BIN
appy/pod/imageNotFound.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 861 B

54
appy/pod/odf_parser.py Normal file
View file

@ -0,0 +1,54 @@
# ------------------------------------------------------------------------------
# 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.'''
def __init__(self, env=None, caller=None):
if not env: env = OdfEnvironment()
XmlParser.__init__(self, env, caller)
# ------------------------------------------------------------------------------

99
appy/pod/parts.py Normal file
View file

@ -0,0 +1,99 @@
# ------------------------------------------------------------------------------
import cgi
# ------------------------------------------------------------------------------
class OdtTable:
'''This class allows to construct an ODT table programmatically. As ODT and
HTML are very similar, this class also allows to contruct an
HTML table.'''
# Some namespace definitions
tns = 'table:'
txns = 'text:'
def __init__(self, name, paraStyle='podTablePara', cellStyle='podTableCell',
nbOfCols=1, paraHeaderStyle=None, cellHeaderStyle=None,
html=False):
# An ODT table must have a name. In the case of an HTML table, p_name
# represents the CSS class for the whole table.
self.name = name
# The default style of every paragraph within cells
self.paraStyle = paraStyle
# The default style of every cell
self.cellStyle = cellStyle
# The total number of columns
self.nbOfCols = nbOfCols
# The default style of every paragraph within a header cell
self.paraHeaderStyle = paraHeaderStyle or paraStyle
# The default style of every header cell
self.cellHeaderStyle = cellHeaderStyle or 'podTableHeaderCell'
# The buffer where the resulting table will be rendered
self.res = ''
# Do we need to generate an HTML table instead of an ODT table ?
self.html = html
def dumpCell(self, content, span=1, header=False,
paraStyle=None, cellStyle=None, align=None):
'''Dumps a cell in the table. If no specific p_paraStyle (p_cellStyle)
is given, self.paraStyle (self.cellStyle) is used, excepted if
p_header is True: in that case, self.paraHeaderStyle
(self.cellHeaderStyle) is used. p_align is used only for HTML.'''
if not paraStyle:
if header: paraStyle = self.paraHeaderStyle
else: paraStyle = self.paraStyle
if not cellStyle:
if header: cellStyle = self.cellHeaderStyle
else: cellStyle = self.cellStyle
if not self.html:
self.res += '<%stable-cell %sstyle-name="%s" ' \
'%snumber-columns-spanned="%d">' % \
(self.tns, self.tns, cellStyle, self.tns, span)
self.res += '<%sp %sstyle-name="%s">%s</%sp>' % \
(self.txns, self.txns, paraStyle,
cgi.escape(str(content)), self.txns)
self.res += '</%stable-cell>' % self.tns
else:
tag = header and 'th' or 'td'
palign = ''
if align: palign = ' align="%s"' % align
self.res += '<%s colspan="%d"%s>%s</%s>' % \
(tag, span, palign, cgi.escape(str(content)), tag)
def startRow(self):
if not self.html:
self.res += '<%stable-row>' % self.tns
else:
self.res += '<tr>'
def endRow(self):
if not self.html:
self.res += '</%stable-row>' % self.tns
else:
self.res += '</tr>'
def startTable(self):
if not self.html:
self.res += '<%stable %sname="%s">' % (self.tns, self.tns,
self.name)
self.res += '<%stable-column %snumber-columns-repeated="%d"/>' % \
(self.tns, self.tns, self.nbOfCols)
else:
css = ''
if self.name: css = ' class="%s"' % self.name
self.res += '<table%s cellpadding="0" cellspacing="0">' % css
def endTable(self):
if not self.html:
self.res += '</%stable>' % self.tns
else:
self.res += '</table>'
def dumpFloat(self, number):
return str(round(number, 2))
def get(self):
'''Returns the whole table.'''
if self.html:
return self.res
else:
return self.res.decode('utf-8')
# ------------------------------------------------------------------------------

382
appy/pod/pod_parser.py Normal file
View file

@ -0,0 +1,382 @@
# ------------------------------------------------------------------------------
# This file is part of Appy, a framework for building applications in the Python
# language. Copyright (C) 2007 Gaetan Delannay
# Appy is free software; you can redistribute it and/or modify it under the
# terms of the GNU General Public License as published by the Free Software
# Foundation; either version 3 of the License, or (at your option) any later
# version.
# Appy 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
# Appy. If not, see <http://www.gnu.org/licenses/>.
# ------------------------------------------------------------------------------
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.items():
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.'''
# 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 sub-buffer 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...)
READING_EXPRESSION = 3 # We are reading a POD expression.
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
# Elements we must ignore (they will not be included in the result)
self.ignorableElems = None # Will be set after namespace propagation
# Elements that may be impacted by POD statements
self.impactableElems = None # Idem
# Elements representing start and end tags surrounding expressions
self.exprStartElems = self.exprEndElems = None # Idem
# 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
# Namespace definitions are not already encountered.
self.gotNamespaces = False
# Store inserts
self.inserts = inserts
# Currently walked "if" actions
self.ifActions = []
# Currently walked named "if" actions
self.namedIfActions = {} #~{s_statementName: IfAction}~
# Currently parsed expression within an ODS template
self.currentOdsExpression = None
self.currentOdsHook = None
# Names of some tags, that we will compute after namespace propagation
self.tags = None
# When an error occurs, must we raise it or write it into he current
# buffer?
self.raiseOnError = None # Will be initialized by PodParser.__init__
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 elemName not in res:
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.currentElem.elem in self.inserts:
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:
colspan = 1
attrSpan = self.tags['number-columns-spanned']
if attrSpan in self.currentElem.attrs:
colspan = int(self.currentElem.attrs[attrSpan])
self.getTable().curColIndex += colspan
elif elem == self.tags['table-column']:
attrs = self.currentElem.attrs
if self.tags['number-columns-repeated'] in attrs:
self.getTable().nbOfColumns += int(
attrs[self.tags['number-columns-repeated']])
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)
# Create a table of names of used tags and attributes (precomputed,
# including namespace, for performance).
table = ns[self.NS_TABLE]
text = ns[self.NS_TEXT]
office = ns[self.NS_OFFICE]
tags = {
'tracked-changes': '%s:tracked-changes' % text,
'change': '%s:change' % text,
'annotation': '%s:annotation' % office,
'change-start': '%s:change-start' % text,
'change-end': '%s:change-end' % text,
'conditional-text': '%s:conditional-text' % text,
'text-input': '%s:text-input' % text,
'table': '%s:table' % table,
'table-name': '%s:name' % table,
'table-cell': '%s:table-cell' % table,
'table-column': '%s:table-column' % table,
'formula': '%s:formula' % table,
'value-type': '%s:value-type' % office,
'value': '%s:value' % office,
'string-value': '%s:string-value' % office,
'span': '%s:span' % text,
'number-columns-spanned': '%s:number-columns-spanned' % table,
'number-columns-repeated': '%s:number-columns-repeated' % table,
}
self.tags = tags
self.ignorableElems = (tags['tracked-changes'], tags['change'])
self.exprStartElems = (tags['change-start'], tags['conditional-text'], \
tags['text-input'])
self.exprEndElems = (tags['change-end'], tags['conditional-text'], \
tags['text-input'])
self.impactableElems = (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)
env.raiseOnError = caller.raiseOnError
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]
tableNs = ns[e.NS_TABLE]
if elem in e.ignorableElems:
e.state = e.IGNORING
elif elem == e.tags['annotation']:
# Be it in an ODT or ODS template, an annotation is considered to
# contain a POD statement.
e.state = e.READING_STATEMENT
elif elem in e.exprStartElems:
# Any track-changed text or being in a conditional or input field is
# considered to be a POD expression.
e.state = e.READING_EXPRESSION
e.exprHasStyle = False
elif (elem == e.tags['table-cell']) and \
e.tags['formula'] in attrs and \
e.tags['value-type'] in attrs and \
(attrs[e.tags['value-type']] == 'string') and \
attrs[e.tags['formula']].startswith('of:="'):
# In an ODS template, any cell containing a formula of type "string"
# and whose content is expressed as a string between double quotes
# (="...") is considered to contain a POD expression. But here it
# is a special case: we need to dump the cell; the expression is not
# directly contained within this cell; the expression will be
# contained in the next inner paragraph. So we must here dump the
# cell, but without some attributes, because the "formula" will be
# converted to the result of evaluating the POD expression.
if e.mode == e.ADD_IN_SUBBUFFER:
e.addSubBuffer()
e.currentBuffer.addElement(e.currentElem.name)
hook = e.currentBuffer.dumpStartElement(elem, attrs,
ignoreAttrs=(e.tags['formula'], e.tags['string-value'],
e.tags['value-type']),
hook=True)
# We already have the POD expression: remember it on the env.
e.currentOdsExpression = attrs[e.tags['string-value']]
e.currentOdsHook = hook
else:
if e.state == e.IGNORING:
pass
elif e.state == e.READING_CONTENT:
if elem in e.impactableElems:
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 == (e.tags['span'])) 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.ignorableElems:
e.state = e.READING_CONTENT
elif elem == e.tags['annotation']:
# 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:
# Dump the ODS POD expression if any
if e.currentOdsExpression:
e.currentBuffer.addExpression(e.currentOdsExpression,
tiedHook=e.currentOdsHook)
e.currentOdsExpression = None
e.currentOdsHook = None
# Dump the ending tag
e.currentBuffer.dumpEndElement(elem)
if elem in e.impactableElems:
if isinstance(e.currentBuffer, MemoryBuffer):
isMainElement = e.currentBuffer.isMainElement(elem)
# Unreference the element among buffer.elements
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,
e.context)
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 in e.exprEndElems:
expression = e.currentContent.strip()
e.currentContent = ''
# Manage expression
e.currentBuffer.addExpression(expression)
if e.exprHasStyle:
e.currentBuffer.dumpEndElement(e.tags['span'])
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:
if e.currentOdsExpression:
# Do not write content if we have encountered an ODS expression:
# we will replace this content with the expression's result.
pass
else:
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
# ------------------------------------------------------------------------------

573
appy/pod/renderer.py Normal file
View file

@ -0,0 +1,573 @@
# ------------------------------------------------------------------------------
# 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, mimetypes, time
#python3 compat
try:
from UserDict import UserDict
except ImportError:
from collections import UserDict
import appy.pod
from appy.pod import PodError
from appy.shared import mimeTypes, mimeTypesExts
from appy.shared.xml_parser import XmlElement
from appy.shared.zip import unzip, zip
from appy.shared.utils import FolderDeleter, executeCommand, FileWrapper
from appy.pod.pod_parser import PodParser, PodEnvironment, OdInsert
from appy.pod.converter import FILE_TYPES
from appy.pod.buffers import FileBuffer
from appy.pod.xhtml2odt import Xhtml2OdtConverter
from appy.pod.doc_importers import \
OdtImporter, ImageImporter, PdfImporter, ConvertImporter, PodImporter
from appy.pod.styles_manager import StylesManager
# ------------------------------------------------------------------------------
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'
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 LibreOffice. ' \
'But the Python interpreter which runs the current script does ' \
'not know UNO, the library that allows to connect to ' \
'LibreOffice 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 LibreOffice port "%s". Make sure it is an integer.'
XHTML_ERROR = 'An error occurred while rendering XHTML content.'
WARNING_INCOMPLETE_OD = 'Warning: your OpenDocument file may not be complete ' \
'(ie imported documents may not be present). This is because we could not ' \
'connect to LibreOffice 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 = open('%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"/>'
# do ... \n from text(...) is obsolete.
OBSOLETE_RENDER_TEXT = 'Obsolete function. Use a pod expression instead ' \
'(field or track-changed). Now, a pod expression ' \
'handles carriage returns and tabs correctly.'
# ------------------------------------------------------------------------------
class Renderer:
templateTypes = ('odt', 'ods') # Types of POD templates
def __init__(self, template, context, result, pythonWithUnoPath=None,
ooPort=2002, stylesMapping={}, forceOoCall=False,
finalizeFunction=None, overwriteExisting=False,
raiseOnError=False, imageResolver=None, stylesTemplate=None):
'''This Python Open Document Renderer (PodRenderer) loads a document
template (p_template) which is an ODT or ODS 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 or .ods, the Renderer will call
LibreOffice to perform a conversion. If p_forceOoCall is True, even
if p_result ends with .odt, LibreOffice 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 LibreOffice. In both cases, we will try to connect to
LibreOffice 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/S result. This
way, you can still perform some actions on the content of the ODT/S
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/S result.
- If you set p_overwriteExisting to True, the renderer will overwrite
the result file. Else, an exception will be thrown if the result file
already exists.
- If p_raiseOnError is False (the default value), any error encountered
during the generation of the result file will be dumped into it, as
a Python traceback within a note. Else, the error will be raised.
- p_imageResolver allows POD to retrieve images, from "img" tags within
XHTML content. Indeed, POD may not be able (ie, may not have the
permission to) perform a HTTP GET on those images. Currently, the
resolver can only be a Zope application object.
- p_stylesTemplate can be the path to a LibreOffice file (ie, a .ott
file) whose styles will be imported within the result.
'''
self.template = 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.env = None
self.pyPath = pythonWithUnoPath
self.ooPort = ooPort
self.forceOoCall = forceOoCall
self.finalizeFunction = finalizeFunction
self.overwriteExisting = overwriteExisting
self.raiseOnError = raiseOnError
self.imageResolver = imageResolver
self.stylesTemplate = stylesTemplate
# Remember potential files or images that will be included through
# "do ... from document" statements: we will need to declare them in
# META-INF/manifest.xml. Keys are file names as they appear within the
# ODT file (to dump in manifest.xml); values are original paths of
# included images (used for avoiding to create multiple copies of a file
# which is imported several times).
self.fileNames = {}
self.prepareFolders()
# Unzip template
self.unzipFolder = os.path.join(self.tempFolder, 'unzip')
os.mkdir(self.unzipFolder)
info = unzip(template, self.unzipFolder, odf=True)
self.contentXml = info['content.xml']
self.stylesXml = info['styles.xml']
self.stylesManager = StylesManager(self.stylesXml)
# From LibreOffice 3.5, it is not possible anymore to dump errors into
# the resulting ods as annotations. Indeed, annotations can't reside
# anymore within paragraphs. ODS files generated with pod and containing
# error messages in annotations cause LibreOffice 3.5 and 4.0 to crash.
# LibreOffice >= 4.1 simply does not show the annotation.
if info['mimetype'] == mimeTypes['ods']: self.raiseOnError = True
# 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,
'text': pe.NS_TEXT}))
self.stylesParser = self.createPodParser('styles.xml', context,
stylesInserts)
# Store the styles mapping
self.setStylesMapping(stylesMapping)
# While working, POD may identify "dynamic styles" to insert into
# the "automatic styles" section of content.xml, like the column styles
# of tables generated from XHTML tables via xhtml2odt.py.
self.dynamicStyles = []
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,
'text': self.renderText,
'test': self.evalIfExpression,
'document': self.importDocument,
'pod': self.importPod,
'pageBreak': self.insertPageBreak} # 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)
# xhtmlString can only be a chunk of XHTML. So we must surround it with
# a tag in order to get a XML-compliant file (we need a root tag).
if xhtmlString == None: xhtmlString = ''
xhtmlContent = '<p>%s</p>' % xhtmlString
return Xhtml2OdtConverter(xhtmlContent, encoding, self.stylesManager,
stylesMapping, self).run()
def renderText(self, text, encoding='utf-8', stylesMapping={}):
'''Obsolete method.'''
raise Exception(OBSOLETE_RENDER_TEXT)
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', 'svg')
ooFormats = ('odt',)
convertibleFormats = list(FILE_TYPES.keys())
def importDocument(self, content=None, at=None, format=None,
anchor='as-char', wrapInPara=True, size=None,
sizeUnit='cm', style=None,
pageBreakBefore=False, pageBreakAfter=False):
'''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, p_wrapInPara and p_size, p_sizeUnit and p_style are only
relevant for images:
* p_anchor defines the way the image is anchored into the document;
Valid values are 'page','paragraph', 'char' and 'as-char';
* p_wrapInPara, if true, wraps the resulting 'image' tag into a 'p'
tag;
* p_size, if specified, is a tuple of float or integers
(width, height) expressing size in p_sizeUnit (see below).
If not specified, size will be computed from image info;
* p_sizeUnit is the unit for p_size elements, it can be "cm"
(centimeters), "px" (pixels) or "pc" (percentage). Percentages, in
p_size, must be expressed as integers from 1 to 100.
* if p_style is given, it is the content of a "style" attribute,
containing CSS attributes. If "width" and "heigth" attributes are
found there, they will override p_size and p_sizeUnit.
p_pageBreakBefore and p_pageBreakAfter are only relevant for import
of external odt documents, and allows to insert a page break
before/after the inserted document.
'''
importer = None
# Is there someting to import?
if not content and not at: raise PodError(DOC_NOT_SPECIFIED)
# Convert Zope files into Appy wrappers.
if content.__class__.__name__ in ('File', 'Image'):
content = FileWrapper(content)
# Guess document format
if isinstance(content, FileWrapper):
format = content.mimeType
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 format in mimeTypesExts:
format = mimeTypesExts[format]
isImage = False
isOdt = False
if format in self.ooFormats:
importer = OdtImporter
self.forceOoCall = True
isOdt = True
elif (format in self.imageFormats) or not format:
# If the format can't be guessed, we suppose it is an image.
importer = ImageImporter
isImage = True
elif format == 'pdf':
importer = PdfImporter
elif format in self.convertibleFormats:
importer = ConvertImporter
else:
raise PodError(DOC_WRONG_FORMAT % format)
imp = importer(content, at, format, self)
# Initialise image-specific parameters
if isImage: imp.init(anchor, wrapInPara, size, sizeUnit, style)
elif isOdt: imp.init(pageBreakBefore, pageBreakAfter)
return imp.run()
def importPod(self, content=None, at=None, format='odt', context=None,
pageBreakBefore=False, pageBreakAfter=False):
'''Similar to m_importDocument, but allows to import the result of
executing the POD template specified in p_content or p_at, and
include it in the POD result.'''
# Is there a pod template defined?
if not content and not at:
raise PodError(DOC_NOT_SPECIFIED)
# If the POD template is specified as a Zope file, convert it into a
# Appy FileWrapper.
if content.__class__.__name__ == 'File':
content = FileWrapper(content)
imp = PodImporter(content, at, format, self)
self.forceOoCall = True
# Define the context to use: either the current context of the current
# POD renderer, or p_context if given.
if context:
ctx = context
else:
ctx = self.contentParser.env.context
imp.init(ctx, pageBreakBefore, pageBreakAfter)
return imp.run()
def insertPageBreak(self):
'''Inserts a page break into the result.'''
textNs = self.currentParser.env.namespaces[PodEnvironment.NS_TEXT]
return '<%s:p %s:style-name="podPageBreak"></%s:p>' % \
(textNs, textNs, textNs)
def prepareFolders(self):
# Check if I can write the result
if not self.overwriteExisting and 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 as oe:
raise PodError(CANT_WRITE_RESULT % (self.result, oe))
except IOError as ie:
raise PodError(CANT_WRITE_RESULT % (self.result, ie))
self.result = os.path.abspath(self.result)
os.remove(self.result)
# Create a temp folder for storing temporary files
absResult = os.path.abspath(self.result)
self.tempFolder = '%s.%f' % (absResult, time.time())
try:
os.mkdir(self.tempFolder)
except OSError as oe:
raise PodError(CANT_WRITE_TEMP_FOLDER % (self.result, oe))
def patchManifest(self):
'''Declares, in META-INF/manifest.xml, images or files included via the
"do... from document" statements if any.'''
if self.fileNames:
j = os.path.join
toInsert = ''
for fileName in self.fileNames.keys():
if fileName.endswith('.svg'):
fileName = os.path.splitext(fileName)[0] + '.png'
mimeType = mimetypes.guess_type(fileName)[0]
toInsert += ' <manifest:file-entry manifest:media-type="%s" ' \
'manifest:full-path="%s"/>\n' % (mimeType, fileName)
manifestName = j(self.unzipFolder, j('META-INF', 'manifest.xml'))
f = file(manifestName)
manifestContent = f.read()
hook = '</manifest:manifest>'
manifestContent = manifestContent.replace(hook, toInsert+hook)
f.close()
# Write the new manifest content
f = file(manifestName, 'w')
f.write(manifestContent)
f.close()
# Public interface
def run(self):
'''Renders the result'''
try:
# 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)
# Patch META-INF/manifest.xml
self.patchManifest()
# Re-zip the result
self.finalize()
finally:
FolderDeleter.delete(self.tempFolder)
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 correspondence 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)
# The predefined styles below are currently ignored, because the
# xhtml2odt parser does not take into account span tags.
if 'span[font-weight=bold]' not in stylesMapping:
stylesMapping['span[font-weight=bold]'] = 'podBold'
if 'span[font-style=italic]' not in stylesMapping:
stylesMapping['span[font-style=italic]'] = 'podItalic'
self.stylesManager.stylesMapping = stylesMapping
except PodError as po:
self.contentParser.env.currentBuffer.content.close()
self.stylesParser.env.currentBuffer.content.close()
if os.path.exists(self.tempFolder):
FolderDeleter.delete(self.tempFolder)
raise po
def callLibreOffice(self, resultName, resultType):
'''Call LibreOffice in server mode to convert or update the result.'''
loOutput = ''
try:
if (not isinstance(self.ooPort, int)) and \
(not isinstance(self.ooPort, int)):
raise PodError(BAD_OO_PORT % str(self.ooPort))
try:
from appy.pod.converter import Converter, ConverterError
try:
Converter(resultName, resultType, self.ooPort,
self.stylesTemplate).run()
except ConverterError as 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 resultName.find(' ') != -1:
qResultName = '"%s"' % resultName
else:
qResultName = resultName
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, qResultName, resultType,
self.ooPort)
if self.stylesTemplate: cmd += ' -t%s' % self.stylesTemplate
loOutput = executeCommand(cmd)
except PodError as pe:
# When trying to call LO in server mode for producing ODT or ODS
# (=forceOoCall=True), if an error occurs we have nevertheless
# an ODT or ODS to return to the user. So we produce a warning
# instead of raising an error.
if (resultType in self.templateTypes) and self.forceOoCall:
print((WARNING_INCOMPLETE_OD % str(pe)))
else:
raise pe
return loOutput
def getTemplateType(self):
'''Identifies the type of the pod template in self.template
(ods or odt). If self.template is a string, it is a file name and we
simply get its extension. Else, it is a binary file in a StringIO
instance, and we seek the mime type from the first bytes.'''
if isinstance(self.template, str):
res = os.path.splitext(self.template)[1][1:]
else:
# A StringIO instance
self.template.seek(0)
firstBytes = self.template.read(90)
firstBytes = firstBytes[firstBytes.index('mimetype')+8:]
if firstBytes.startswith(mimeTypes['ods']):
res = 'ods'
else:
# We suppose this is ODT
res = 'odt'
return res
def finalize(self):
'''Re-zip the result and potentially call LibreOffice if target format
is not among self.templateTypes or if forceOoCall is True.'''
for innerFile in ('content.xml', 'styles.xml'):
shutil.copy(os.path.join(self.tempFolder, innerFile),
os.path.join(self.unzipFolder, innerFile))
# Insert dynamic styles
contentXml = os.path.join(self.unzipFolder, 'content.xml')
f = file(contentXml)
dynamicStyles = ''.join(self.dynamicStyles)
content = f.read().replace('<!DYNAMIC_STYLES!>', dynamicStyles)
f.close()
f = file(contentXml, 'w')
f.write(content)
f.close()
# Call the user-defined "finalize" function when present
if self.finalizeFunction:
try:
self.finalizeFunction(self.unzipFolder)
except Exception as e:
print((WARNING_FINALIZE_ERROR % str(e)))
# Re-zip the result, first as an OpenDocument file of the same type as
# the POD template (odt, ods...)
resultExt = self.getTemplateType()
resultName = os.path.join(self.tempFolder, 'result.%s' % resultExt)
zip(resultName, self.unzipFolder, odf=True)
resultType = os.path.splitext(self.result)[1].strip('.')
if (resultType in self.templateTypes) and not self.forceOoCall:
# Simply move the ODT result to the result
os.rename(resultName, self.result)
else:
if resultType not in FILE_TYPES:
raise PodError(BAD_RESULT_TYPE % (
self.result, FILE_TYPES.keys()))
# Call LibreOffice to perform the conversion or document update.
output = self.callLibreOffice(resultName, resultType)
# I (should) have the result. Move it to the correct name.
resPrefix = os.path.splitext(resultName)[0]
if resultType in self.templateTypes:
# converter.py has (normally!) created a second file
# suffixed .res.[resultType]
finalResultName = '%s.res.%s' % (resPrefix, resultType)
if not os.path.exists(finalResultName):
finalResultName = resultName
# In this case OO in server mode could not be called to
# update indexes, sections, etc.
else:
finalResultName = '%s.%s' % (resPrefix, resultType)
if not os.path.exists(finalResultName):
raise PodError(CONVERT_ERROR % output)
os.rename(finalResultName, self.result)
# ------------------------------------------------------------------------------

View file

@ -0,0 +1,147 @@
<@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="podUnderline" @style@:family="text">
<@style@:text-properties @style@:text-underline-style="solid" @style@:text-underline-width="auto"
@style@:text-underline-color="font-color"/>
</@style@:style>
<@style@:style @style@:name="podStrike" @style@:family="text">
<@style@:text-properties @style@:text-line-through-style="solid" @style@:text-line-through-width="auto"
@style@:text-line-through-color="font-color"/>
</@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="podPageBreak" @style@:family="paragraph">
<@style@:paragraph-properties @fo@:break-before="page"/>
</@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>
<@style@:style @style@:name="podImageLeft" @style@:family="graphic" @style@:parent-style-name="Graphics">
<@style@:graphic-properties @style@:run-through="foreground" @style@:wrap="parallel" @style@:number-wrapped-paragraphs="no-limit" @style@:wrap-contour="false" @style@:vertical-pos="top" @style@:vertical-rel="paragraph" @style@:horizontal-pos="left" @style@:horizontal-rel="paragraph" @style@:mirror="none" @fo@:clip="rect(0cm, 0cm, 0cm, 0cm)" @fo@:margin-right="0.3cm" @fo@:margin-bottom="0.2cm"/>
</@style@:style>
<@style@:style @style@:name="podImageRight" @style@:family="graphic" @style@:parent-style-name="Graphics">
<@style@:graphic-properties @style@:run-through="foreground" @style@:wrap="parallel" @style@:number-wrapped-paragraphs="no-limit" @style@:wrap-contour="false" @style@:vertical-pos="top" @style@:vertical-rel="paragraph" @style@:horizontal-pos="right" @style@:horizontal-rel="paragraph" @style@:mirror="none" @fo@:clip="rect(0cm, 0cm, 0cm, 0cm)" @fo@:margin-left="0.3cm" @fo@:margin-bottom="0.2cm"/>
</@style@:style>
<@style@:style @style@:name="podTablePara" @style@:family="paragraph" @style@:parent-style-name="Appy_Table_Content">
<@style@:text-properties @fo@:font-size="8pt" @fo@:font-weight="normal" @style@:font-weight-asian="normal" @style@:font-weight-complex="normal"/>
</@style@:style>
<@style@:style @style@:name="podTableParaBold" @style@:family="paragraph" @style@:parent-style-name="Appy_Table_Content">
<@style@:text-properties @fo@:font-size="8pt" @fo@:font-weight="bold" @style@:font-weight-asian="bold" @style@:font-weight-complex="bold"/>
</@style@:style>
<@style@:style @style@:name="podTableParaRight" @style@:family="paragraph" @style@:parent-style-name="Appy_Table_Content">
<@style@:paragraph-properties @fo@:text-align="end" @style@:justify-single-word="false"/>
<@style@:text-properties @fo@:font-size="8pt" @fo@:font-weight="normal" @style@:font-weight-asian="normal" @style@:font-weight-complex="normal"/>
</@style@:style>
<@style@:style @style@:name="podTableParaBoldRight" @style@:family="paragraph" @style@:parent-style-name="Appy_Table_Content">
<@style@:paragraph-properties @fo@:text-align="end" @style@:justify-single-word="false"/>
<@style@:text-properties @fo@:font-size="8pt" @fo@:font-weight="bold" @style@:font-weight-asian="bold" @style@:font-weight-complex="bold"/>
</@style@:style>
<@style@:style @style@:name="podTableCell" @style@:family="table-cell">
<@style@:table-cell-properties @fo@:padding="0.097cm" @fo@:border="0.018cm solid #000000"/>
</@style@:style>
<@style@:style @style@:name="podTableHeaderCell" @style@:family="table-cell">
<@style@:table-cell-properties @fo@:background-color="#e6e6e6" @fo@:padding="0.097cm" @fo@:border="0.018cm solid #000000">
<@style@:background-image/>
</@style@:table-cell-properties>
</@style@:style>
<!DYNAMIC_STYLES!>

View file

@ -0,0 +1,18 @@
<@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>
<@style@:style style:name="AppyStandard" style:family="paragraph" style:class="text" style:master-page-name="" @style@:parent-style-name="Standard">
<@style@:paragraph-properties fo:margin-left="0cm" fo:margin-right="0cm" fo:margin-top="0.101cm" fo:margin-bottom="0.169cm" fo:text-indent="0cm" style:auto-text-indent="false" style:page-number="auto"/>
</@style@:style>
<@style@:style @style@:name="Appy_Table_Content" @style@:display-name="Appy Table Contents" @style@:family="paragraph"
@style@:parent-style-name="AppyStandard" @style@:class="extra">
<@style@:paragraph-properties @fo@:margin-top="0cm" @fo@:margin-bottom="0cm" @text@:number-lines="false" @text@:line-number="0"/>
</@style@:style>
<@style@:style @style@:name="Appy_Table_Heading" @style@:display-name="Appy Table Heading" @style@:family="paragraph"
@style@:parent-style-name="Appy_Table_Contents" @style@:class="extra">
<@style@:paragraph-properties @fo@:text-align="center" @style@:justify-single-word="false" @text@:number-lines="false" @text@:line-number="0"/>
<@style@:text-properties @fo@:font-weight="bold" @style@:font-weight-asian="bold" @style@:font-weight-complex="bold"/>
</@style@:style>

405
appy/pod/styles_manager.py Normal file
View file

@ -0,0 +1,405 @@
# ------------------------------------------------------------------------------
# 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
#python3 compat
try:
from UserDict import UserDict
except ImportError:
from collections import UserDict
import appy.pod
from appy.pod import *
from appy.pod.odf_parser import OdfEnvironment, OdfParser
from appy.shared.css import parseStyleAttribute
# 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.values():
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.values():
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 = list(self.values())
else:
for style in self.values():
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 classAttr in attrs:
style.styleClass = attrs[classAttr]
if displayNameAttr in attrs:
style.displayName = attrs[displayNameAttr]
# Record this style in the environment
e.styles[style.name] = style
e.currentStyle = style
levelKey = '%s:default-outline-level' % e.ns(e.NS_STYLE)
if levelKey in attrs and attrs[levelKey].strip():
style.outlineLevel = int(attrs[levelKey])
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 fontSizeKey in attrs:
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 may 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, and returns the
internal representation of it. p_stylesMapping is a dict where:
* every key can be:
(1) the name of a XHTML 'paragraph-like' tag (p, h1, h2...)
(2) the name of a XHTML 'text-like' tag (span, b, i, em...)
(3) the name of a CSS class
(4) string 'h*'
* every value must be:
(a) if the key is (1), (2) or (3), value must be the display name
of an ODT style
(b) if the key is (4), value must be an integer indicating how to
map the outline level of outlined styles (ie, for mapping XHTML
tag "h1" to the OD style with outline-level=2, value must be
integer "1". In that case, h2 will be mapped to the ODT style
with outline-level=3, etc.). Note that this value can also be
negative.
* Some precision now about about keys. If key is (1) or (2),
parameters can be given between square brackets. Every such
parameter represents a CSS attribute and its value. For example, a
key can be:
p[text-align=center,color=blue]
This feature allows to map XHTML tags having different CSS
attributes to different ODT styles.
The method returns a dict which is the internal representation of
the styles mapping:
* every key can be:
(I) the name of a XHTML tag, corresponding to (1) or (2) whose
potential parameters have been removed;
(II) the name of a CSS class (=(3))
(III) string 'h*' (=(4))
* every value can be:
(i) a Styles instance that was found from the specified ODT style
display name in p_stylesMapping, if key is (I) and if only one,
non-parameterized XHTML tag was defined in p_stylesMapping;
(ii) a list of the form [ (params, Style), (params, Style),...]
if key is (I) and if one or more parameterized (or not) XHTML
tags representing the same tag were found in p_stylesMapping.
params, which can be None, is a dict whose pairs are of the
form (cssAttribute, cssValue).
(iii) an integer value (=(b)).
'''
res = {}
if not isinstance(stylesMapping, dict) and \
not isinstance(stylesMapping, UserDict):
raise PodError(MAPPING_NOT_DICT)
for xhtmlStyleName, odtStyleName in stylesMapping.items():
if not isinstance(xhtmlStyleName, str):
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, str):
raise PodError(MAPPING_ELEM_NOT_STRING)
if (xhtmlStyleName != 'h*') and \
((not xhtmlStyleName) or (not odtStyleName)):
raise PodError(MAPPING_ELEM_EMPTY)
# Separate CSS attributes if any
cssAttrs = None
if '[' in xhtmlStyleName:
xhtmlStyleName, attrs = xhtmlStyleName.split('[')
xhtmlStyleName = xhtmlStyleName.strip()
attrs = attrs.strip()[:-1].split(',')
cssAttrs = {}
for attr in attrs:
name, value = attr.split('=')
cssAttrs[name.strip()] = value.strip()
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 odtStyleName in self.podSpecificStyles:
odtStyle = self.podSpecificStyles[odtStyleName]
else:
raise PodError(STYLE_NOT_FOUND % odtStyleName)
self.checkStylesAdequation(xhtmlStyleName, odtStyle)
# Store this style mapping in the result.
alreadyInRes = xhtmlStyleName in res
if cssAttrs or alreadyInRes:
# I must create a complex structure (ii) for this mapping.
if not alreadyInRes:
res[xhtmlStyleName] = [(cssAttrs, odtStyle)]
else:
value = res[xhtmlStyleName]
if not isinstance(value, list):
res[xhtmlStyleName] = [(cssAttrs, odtStyle), \
(None, value)]
else:
res.insert(0, (cssAttrs, odtStyle))
else:
# I must create a simple structure (i) for this mapping.
res[xhtmlStyleName] = odtStyle
else:
# In this case (iii), it is the outline level, not an ODT style
# name.
res[xhtmlStyleName] = odtStyleName
return res
def styleMatch(self, attrs, matchingAttrs):
'''p_attrs is a dict of attributes found on some HTML element.
p_matchingAttrs is a dict of attributes corresponding to some style.
This method returns True if p_attrs contains the winning (name,value)
pairs that match those in p_matchingAttrs. Note that ALL attrs in
p_matchingAttrs must be present in p_attrs.'''
for name, value in matchingAttrs.items():
if name not in attrs: return
if value != attrs[name]: return
return True
def getStyleFromMapping(self, elem, attrs, styles):
'''p_styles is a Style instance or a list of (cssParams, Style) tuples.
Depending on CSS attributes found in p_attrs, this method returns
the relevant Style instance.'''
if isinstance(styles, Style): return styles
hasStyleInfo = attrs and ('style' in attrs)
if not hasStyleInfo:
# If I have, at the last position in p_styles, the style related to
# no attribute at all, I return it.
lastAttrs, lastStyle = styles[-1]
if lastAttrs == None: return lastStyle
else: return
# If I am here, I have style info. Check if it corresponds to some style
# in p_styles.
styleInfo = parseStyleAttribute(attrs['style'], asDict=True)
for matchingAttrs, style in styles:
if self.styleMatch(styleInfo, matchingAttrs):
return style
def findStyle(self, elem, attrs, classValue, localStylesMapping):
'''Finds the ODT style that must be applied to XHTML p_elem that has
attrs p_attrs. In some cases, p_attrs is None; the value of the
"class" attribute is given instead (in p_classValue).
The global styles mapping is in self.stylesMapping; the local styles
mapping is in p_localStylesMapping.
Here are the places where we will search, ordered by
priority (highest first):
(1) local styles mapping (CSS style in "class" attr)
(2) " (HTML elem)
(3) global styles mapping (CSS style in "class" attr)
(4) " (HTML elem)
(5) ODT style that has the same name as CSS style in "class" attr
(6) Predefined pod-specific ODT style that has the same name as
CSS style in "class" attr
(7) ODT style that has the same outline level as HTML elem.
'''
res = None
cssStyleName = None
if attrs and 'class' in attrs:
cssStyleName = attrs['class']
if classValue:
cssStyleName = classValue
# (1)
if cssStyleName in localStylesMapping:
res = localStylesMapping[cssStyleName]
# (2)
if (not res) and elem in localStylesMapping:
styles = localStylesMapping[elem]
res = self.getStyleFromMapping(elem, attrs, styles)
# (3)
if (not res) and cssStyleName in self.stylesMapping:
res = self.stylesMapping[cssStyleName]
# (4)
if (not res) and elem in self.stylesMapping:
styles = self.stylesMapping[elem]
res = self.getStyleFromMapping(elem, attrs, styles)
# (5)
if (not res) and cssStyleName in self.styles:
res = self.styles[cssStyleName]
# (6)
if (not res) and cssStyleName in self.podSpecificStyles:
res = self.podSpecificStyles[cssStyleName]
# (7)
if not res:
# Try to find a style with the correct outline level
if elem in XHTML_HEADINGS:
# Is there a delta that must be taken into account ?
outlineDelta = 0
if 'h*' in localStylesMapping:
outlineDelta += localStylesMapping['h*']
elif 'h*' in self.stylesMapping:
outlineDelta += self.stylesMapping['h*']
outlineLevel = int(elem[1]) + outlineDelta
# Normalize the outline level
if outlineLevel < 1: outlineLevel = 1
res = self.styles.getParagraphStyleAtLevel(outlineLevel)
if res:
self.checkStylesAdequation(elem, res)
return res
# ------------------------------------------------------------------------------

16
appy/pod/test/Readme.txt Normal file
View file

@ -0,0 +1,16 @@
Here you will find some ODT documents that are POD templates.
A POD template is a standard ODT file, where:
- notes are used to insert Python-based code for telling POD to render
a portion of the document zero, one or more times ("if" and "for" statements);
- text insertions in "track changes" mode are interpreted as Python expressions.
When you run 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 !

234
appy/pod/test/Tester.py Normal file
View file

@ -0,0 +1,234 @@
# ------------------------------------------------------------------------------
# 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.shared.xml_parser import escapeXml
from appy.pod.odf_parser import OdfEnvironment, OdfParser
from appy.pod.renderer import Renderer
# 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 = ''
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 list(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: self.res += escapeXml(content)
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
if self.data['Template'].endswith('.ods'):
suffix = ''
else:
# For ODT, which is the most frequent case, no need to specify the
# file extension.
suffix = '.odt'
template = os.path.join(self.templatesFolder,
self.data['Template'] + suffix)
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 %s, temp folder 2 is %s.' % (self.result,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()
try:
f.write(fileContent.encode('utf-8'))
except UnicodeDecodeError:
f.write(fileContent)
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'] + '.' + self.data['Result'])
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','text:bullet-char',
'table:name', 'table:style-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()
# ------------------------------------------------------------------------------

1895
appy/pod/test/Tests.rtf Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1 @@

View file

@ -0,0 +1,5 @@
johnScore = 25
markScore = 53
wilsonScore = 12
meghuScore = 59

View file

@ -0,0 +1,137 @@
# -*- coding: utf-8 -*-
xhtmlInput='''
<table cellspacing="0" cellpadding="0" id="configabsences_cal" class="list timeline">
<colgroup>
<col width="100px"/>
<col style=""/><col style=""/><col style=""/><col style=""/><col style="background-color: #dedede"/><col style="background-color: #c0c0c0"/><col style="background-color: #c0c0c0"/><col style=""/><col style=""/><col style=""/><col style=""/><col style="background-color: #dedede"/><col style="background-color: #c0c0c0"/><col style="background-color: #c0c0c0"/><col style=""/><col style=""/><col style=""/><col style=""/><col style="background-color: #dedede"/><col style="background-color: #c0c0c0"/><col style="background-color: #c0c0c0"/><col style=""/><col style=""/><col style=""/><col style=""/><col style="background-color: #dedede"/><col style="background-color: #c0c0c0"/><col style="background-color: #c0c0c0"/><col style=""/><col style=""/><col style=""/><col style=""/><col style="background-color: #dedede"/><col style="background-color: #c0c0c0"/><col style="background-color: #c0c0c0"/><col style=""/><col style=""/><col style=""/><col style=""/><col style="background-color: #dedede"/><col style="background-color: #c0c0c0"/><col style="background-color: #c0c0c0"/>
<col style="width: 75px"/>
</colgroup>
<tbody>
<tr><th></th>
<th colspan="6">Février 2015</th><th colspan="31">Mars 2015</th><th colspan="5"><acronym title="Avril 2015">A</acronym></th><th></th></tr>
<tr><td></td> <td><b>L</b></td><td><b>M</b></td><td><b>M</b></td><td><b>J</b></td><td><b>V</b></td><td><b>S</b></td><td><b>D</b></td><td><b>L</b></td><td><b>M</b></td><td><b>M</b></td><td><b>J</b></td><td><b>V</b></td><td><b>S</b></td><td><b>D</b></td><td><b>L</b></td><td><b>M</b></td><td><b>M</b></td><td><b>J</b></td><td><b>V</b></td><td><b>S</b></td><td><b>D</b></td><td><b>L</b></td><td><b>M</b></td><td><b>M</b></td><td><b>J</b></td><td><b>V</b></td><td><b>S</b></td><td><b>D</b></td><td><b>L</b></td><td><b>M</b></td><td><b>M</b></td><td><b>J</b></td><td><b>V</b></td><td><b>S</b></td><td><b>D</b></td><td><b>L</b></td><td><b>M</b></td><td><b>M</b></td><td><b>J</b></td><td><b>V</b></td><td><b>S</b></td><td><b>D</b></td>
<td></td></tr>
<tr><td></td> <td><b>23</b></td><td><b>24</b></td><td><b>25</b></td><td><b>26</b></td><td><b>27</b></td><td><b>28</b></td><td><b>01</b></td><td><b>02</b></td><td><b>03</b></td><td><b>04</b></td><td><b>05</b></td><td><b>06</b></td><td><b>07</b></td><td><b>08</b></td><td><b>09</b></td><td><b>10</b></td><td><b>11</b></td><td><b>12</b></td><td><b>13</b></td><td><b>14</b></td><td><b>15</b></td><td><b>16</b></td><td><b>17</b></td><td><b>18</b></td><td><b>19</b></td><td><b>20</b></td><td><b>21</b></td><td><b>22</b></td><td><b>23</b></td><td><b>24</b></td><td><b>25</b></td><td><b>26</b></td><td><b>27</b></td><td><b>28</b></td><td><b>29</b></td><td><b>30</b></td><td><b>31</b></td><td><b>01</b></td><td><b>02</b></td><td><b>03</b></td><td><b>04</b></td><td><b>05</b></td>
<td></td></tr>
<tr>
<td class="tlLeft"><a target="_blank" href="http://localhost:8080/config/GardesUser1350721915326376433960017169/view?page=preferences"><acronym title="Barbason Alain">AB</acronym></a></td>
<td style="background-color: #E5B620"></td>
<td style="background-color: #E5B620"></td>
<td style="background-color: #E5B620"></td>
<td></td>
<td style="background-color: #E5B620"></td>
<td style="background-color: #E5B620"></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td style="background-color: #13751F"></td>
<td style="background-color: #13751F"></td>
<td></td>
<td></td>
<td title="Congé (AM), Congrès (PM)" style="background-image: url(http://localhost:8080/ui/angled.png)"></td>
<td></td>
<td></td>
<td style="background-color: #d08181"></td>
<td style="background-color: #d08181"></td>
<td style="background-color: #d08181"></td>
<td style="background-color: #d08181"></td>
<td style="background-color: #d08181"></td>
<td style="background-color: #d08181"></td>
<td style="background-color: #d08181"></td>
<td style="background-color: #d08181"></td>
<td style="background-color: #d08181"></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td class="tlRight"><a target="_blank" href="http://localhost:8080/config/GardesUser1350721915326376433960017169/view?page=preferences"><acronym title="Barbason Alain">AB</acronym></a></td>
</tr><tr>
<td class="tlLeft"><a target="_blank" href="http://localhost:8080/config/GardesUser1350721915119231834005028903/view?page=preferences"><acronym title="Blom-Peters Lucien">LB</acronym></a></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td style="background-color: #d08181"></td>
<td style="background-color: #d08181"></td>
<td style="background-color: #d08181"></td>
<td style="background-color: #d08181"></td>
<td style="background-color: #d08181"></td>
<td style="background-color: #d08181"></td>
<td style="background-color: #d08181"></td>
<td style="background-color: #d08181"></td>
<td style="background-color: #d08181"></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td class="tlRight"><a target="_blank" href="http://localhost:8080/config/GardesUser1350721915119231834005028903/view?page=preferences"><acronym title="Blom-Peters Lucien">LB</acronym></a></td>
</tr>
</tbody>
<tbody id="configabsences_trs">
<script>new AjaxData('configabsences_trs', 'absences:pxTotalRowsFromAjax', {}, 'configabsences')</script>
<tr>
<td class="tlLeft">
<acronym title="Nombre de travailleurs disponibles"><b>P</b></acronym></td>
<td>42</td><td>42</td><td>41</td><td>42</td><td>41</td><td>39</td><td>40</td><td>42</td><td>41</td><td>41</td><td>41</td><td>42</td><td>37</td><td>37</td><td>41</td><td>42</td><td>40</td><td>39</td><td>39</td><td>37</td><td>37</td><td>39</td><td>37</td><td>36</td><td>36</td><td>36</td><td>31</td><td>32</td><td>38</td><td>39</td><td>39</td><td>39</td><td>38</td><td>37</td><td>37</td><td>42</td><td>41</td><td>41</td><td>41</td><td>42</td><td>33</td><td>33</td>
<td class="tlRight">
<acronym title="Nombre de travailleurs disponibles"><b>P</b></acronym></td>
</tr><tr>
<td class="tlLeft">
<acronym title="Nombre total de travailleurs"><b>T</b></acronym></td>
<td>46</td><td>46</td><td>46</td><td>46</td><td>46</td><td>46</td><td>46</td><td>46</td><td>46</td><td>46</td><td>46</td><td>46</td><td>46</td><td>46</td><td>46</td><td>46</td><td>46</td><td>46</td><td>46</td><td>46</td><td>46</td><td>46</td><td>46</td><td>46</td><td>46</td><td>46</td><td>46</td><td>46</td><td>46</td><td>46</td><td>46</td><td>46</td><td>46</td><td>46</td><td>46</td><td>46</td><td>46</td><td>46</td><td>46</td><td>46</td><td>46</td><td>46</td>
<td class="tlRight">
<acronym title="Nombre total de travailleurs"><b>T</b></acronym></td>
</tr>
</tbody>
<tbody>
<tr><td></td>
<td><b>23</b></td><td><b>24</b></td><td><b>25</b></td><td><b>26</b></td><td><b>27</b></td><td><b>28</b></td><td><b>01</b></td><td><b>02</b></td><td><b>03</b></td><td><b>04</b></td><td><b>05</b></td><td><b>06</b></td><td><b>07</b></td><td><b>08</b></td><td><b>09</b></td><td><b>10</b></td><td><b>11</b></td><td><b>12</b></td><td><b>13</b></td><td><b>14</b></td><td><b>15</b></td><td><b>16</b></td><td><b>17</b></td><td><b>18</b></td><td><b>19</b></td><td><b>20</b></td><td><b>21</b></td><td><b>22</b></td><td><b>23</b></td><td><b>24</b></td><td><b>25</b></td><td><b>26</b></td><td><b>27</b></td><td><b>28</b></td><td><b>29</b></td><td><b>30</b></td><td><b>31</b></td><td><b>01</b></td><td><b>02</b></td><td><b>03</b></td><td><b>04</b></td><td><b>05</b></td>
<td></td></tr>
<tr><td></td>
<td><b>L</b></td><td><b>M</b></td><td><b>M</b></td><td><b>J</b></td><td><b>V</b></td><td><b>S</b></td><td><b>D</b></td><td><b>L</b></td><td><b>M</b></td><td><b>M</b></td><td><b>J</b></td><td><b>V</b></td><td><b>S</b></td><td><b>D</b></td><td><b>L</b></td><td><b>M</b></td><td><b>M</b></td><td><b>J</b></td><td><b>V</b></td><td><b>S</b></td><td><b>D</b></td><td><b>L</b></td><td><b>M</b></td><td><b>M</b></td><td><b>J</b></td><td><b>V</b></td><td><b>S</b></td><td><b>D</b></td><td><b>L</b></td><td><b>M</b></td><td><b>M</b></td><td><b>J</b></td><td><b>V</b></td><td><b>S</b></td><td><b>D</b></td><td><b>L</b></td><td><b>M</b></td><td><b>M</b></td><td><b>J</b></td><td><b>V</b></td><td><b>S</b></td><td><b>D</b></td>
<td></td></tr>
<tr><th></th>
<th colspan="6">Février 2015</th><th colspan="31">Mars 2015</th><th colspan="5"><acronym title="Avril 2015">A</acronym></th><th></th></tr>
</tbody>
</table>
'''

View file

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

View file

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

View file

@ -0,0 +1,2 @@
old = 'OLD'
new = 'NEW'

View file

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

View file

@ -0,0 +1,17 @@
class Student:
def __init__(self, **kwargs):
for k, v in kwargs.items():
setattr(self, k, v)
students = [
Student(parent_guardian='Parent 1', street='Street 1', city='Flawinne',
state='Namur', zip='5020', lname='Name 1', fname='First name 1'),
Student(parent_guardian='Parent 2', street='Street 2', city='Flawinne',
state='Namur', zip='5020', lname='Name 2', fname='First name 2'),
Student(parent_guardian='Parent 3', street='Street 3', city='Flawinne',
state='Namur', zip='5020', lname='Name 3', fname='First name 3'),
Student(parent_guardian='Parent 4', street='Street 4', city='Flawinne',
state='Namur', zip='5020', lname='Name 4', fname='First name 4'),
Student(parent_guardian='Parent 5', street='Street 5', city='Flawinne',
state='Namur', zip='5020', lname='Name 5', fname='First name 5'),
]

View file

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

View file

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

View file

@ -0,0 +1,4 @@
data = [ \
['1', 2, 'three'],
['A', 'BB', 'CCC']
]

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,2 @@
var1 = 'VAR1 not overridden'
var2 = 'VAR2 not overridden'

View file

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

View file

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

View file

@ -0,0 +1,95 @@
# -*- coding: utf-8 -*-
xhtmlInput = '''
<ol><li>
<p>Test du champ kupu<br />A la ligne
1, j'y suis</p>
<ol><li>
<p>Ligne 1 de 1<br />A la ligne 1 de 1
fdsfds fsd fsd fksdf sdfs dfd sfsd fsd fsd fdsf dsfds fsdfa azra
zeeamr earkl kfl flks dlfksd lfklsd fklmsdfkl dskflmdsk flmsdf
lmdsflm dflsdfs fksd fmlsd flmdsk flmdsf mlsfkmls<br />A la ligne 2
de 1 cds fdsn sfd dsjfl dsjfhjds fhjdsf lqdhf klsfql kjfk jfkj
qfklmqds fjdlksqfj kdlfj klqfk qfjk ljfqklq djfklqjf qfk jkfljqd
sklfjqdklfjqdkl fjdqklf jdqlkfj qfq</p>
</li><li>
<p>Ligne 2 de 1<br />A la ligne 1 de 2
fdsfds fsd fsd fksdf sdfs dfd sfsd fsd fsd fdsf dsfds fsdfa azra
zeeamr earkl kfl flks dlfksd lfklsd fklmsdfkl dskflmdsk flmsdf
lmdsflm dflsdfs fksd fmlsd flmdsk flmdsf mlsfkmls<br />A la ligne 2
de 2 cds fdsn sfd dsjfl dsjfhjds fhjdsf lqdhf klsfql kjfk jfkj
qfklmqds fjdlksqfj kdlfj klqfk qfjk ljfqklq djfklqjf qfk jkfljqd
sklfjqdklfjqdkl fjdqklf jdqlkfj qf</p>
</li></ol>
</li><li>
<p>Ligne 2 tout court</p>
<ol><li>
<p>Ligne bullet dg fg dgd fgdf gdfg
dgq fg fgfq gfqd gfqg qfg qgkqlglk lgkl fkgkfq lmgkl mfqfkglmfk
gmlqf gmlqfgml kfmglk qmglk qmlgk qmlgkqmflgk qmlqg fmdlmslgk
mlsgml fskfmglk gmlkflmg ksfmlgk mlsgk</p>
</li><li>
<p>dsfl kqfs dmflm dsfsdf lskfmls
dkflmsdkf sdlmkf dslmfk sdmlfksd mlfksdmfl ksflmksdflmd slfmskd
lsmlfk mlsdfkl mskfmlsfk lmskfsfs</p>
</li><li>
<p>fmlsdm ùfkùds fldsf ùsfsdmfù
mdsfù msdùfms</p>
</li><li>
<p>fds fsd fdsf sdfds fsmd fmjdfklm
sdflmkd lfqlmklmdsqkflmq dskflmkd slmgkqdfmglklfmgksmldgk
dmlsgdkdlm fgkmdl fkgdmlfsgk mlfgksmdl fgkmldsf klmmdfkg mlsdfkgml
skfdgml skgmlkfd smlgksd mlkgml kml</p>
</li><li>
<p>lgd ksmlgjk mlsdfgkml sdfkglm
kdsfmlgk dlmskgsldmgk lms</p>
</li></ol>
</li><li>
<p>Ligne 3 tout court</p>
</li></ol>
<br />'''
xhtmlInput2 = '''
<ol start="1"><li>Le Gouvernement approuve la réaffectation de 20.056.989 attribués dans le cadre du CRAC II et laffectation du solde de 20.855.107 du solde des CRAC Ibis et II au sein du secteur des hôpitaux de lenveloppe de financement alternatif pour un montant total de 40.921.096 , au bénéfice des établissements suivants :<br /><br /></li>
<table align="center">
<tbody>
<tr>
<td>
<b>Etablissement</b></td>
<td>
<p align="center" style="text-align: center;"><b>CRAC II Phase II</b></p>
</td>
</tr>
<tr>
<td>
<p>C.H. Chrétien Liège</p>
</td>
<td nowrap="-1">
<p align="center" style="text-align: center;">11.097.377</p>
</td>
</tr>
<tr>
<td nowrap="-1">
<p>Hôp. St-Joseph, Ste-Thérèse et IMTR Gilly</p>
</td>
<td nowrap="-1">
<p align="center" style="text-align: center;">8.297.880</p>
</td>
</tr>
</tbody>
</table>
<br /><li>Il prend acte des décisions du Ministre de la Santé relatives à loctroi de la règle de 10/90 pour la subsidiation des infrastructures des établissements concernés.<br /></li>
<li>Le Gouvernement charge le Ministre de la Santé dappliquer, le cas échéant, les nouveaux plafonds à la construction visés dans larrêté ministériel du 11 mai 2007 fixant le coût maximal pouvant être pris en considération pour loctroi des subventions pour la construction de nouveaux bâtiments, les travaux dextension et de reconditionnement dun hôpital ou dun service, aux demandes doctroi de subventions antérieures au 1<sup>er</sup> janvier 2006, pour autant que ces demandes doctroi de subventions naient pas encore donné lieu à lexploitation à cette même date.<br /></li>
<li>Il charge le Ministre de la Santé de lexécution de la présente décision</li></ol>
<p></p>
'''
xhtmlInput3 = '''
<ol><li>Le Gouvernement l'exercice 2008.</li><li>Il approuve 240.000€ de007-2008.</li><li>Le Gouvernement approuve:
<ul><li>le projet d'arrêté ministériel 008;</li><li>le projet d'arrêté ministériel mique 2008-2009.</li></ul></li><li>Le Gouvernement charge le Ministre de l'Economie de l'exécution de la présente décision.</li></ol>
'''
xhtmlInput4 = '''
<div><strong>Programmes FSE Convergence et Compétitivité régionale et emploi.</strong></div>
<div><strong>Axe 1, mesure 1, et Axe 2, mesures 2, 3, 4 et 5 : formation.</strong></div>
<div><strong>Portefeuille de projets « Enseignement supérieur - Formation continue ».</strong></div>
'''

View file

@ -0,0 +1,44 @@
# -*- coding: utf-8 -*-
xhtmlInput = '''
<p>Champ FCK</p>
<ol>
<li>aaaa
<ol>
<li>Azerty</li>
<li>aaaa</li>
</ol>
</li>
<li>edzfrgh</li>
<li>Kupu</li>
</ol>
<table cellspacing="1" cellpadding="1" border="1" style="width: 210px; height: 66px;">
<tbody>
<tr>
<td>a</td>
<td>b</td>
</tr>
<tr>
<td>x</td>
<td>vvv</td>
</tr>
<tr>
<td>bbb</td>
<td>vvvvv</td>
</tr>
</tbody>
</table>
<p>&nbsp;</p>
<p style="margin-left: 40px;">hghdghghgh</p>
<ul>
<li>aaa</li>
<li>&nbsp;</li>
<li>bvbb</li>
</ul>
<ol>
<li>regrg</li>
<li>&nbsp;</li>
</ol>
<p>vghriqghrghgfd&nbsp; hgkll hgjkf lghjfkd slhgjfd klhgjfds klghjfds s&amp;é@èù&nbsp; vghriqghrghgfd&nbsp; hgkll hgjkf lghjfkd slhgjfd klhgjfds klghjfds s&amp;é@èù vghriqghrghgfd&nbsp; hgkll hgjkf lghjfkd slhgjfd klhgjfds klghjfds s&amp;é@èù vghriqghrghgfd&nbsp; hgkll hgjkf lghjfkd slhgjfd klhgjfds klghjfds s&amp;é@èù vghriqghrghgfd&nbsp; hgkll hgjkf lghjfkd slhgjfd klhgjfds klghjfds s&amp;é@èù vghriqghrghgfd&nbsp; hgkll hgjkf lghjfkd slhgjfd klhgjfds klghjfds s&amp;é@èù vghriqghrghgfd&nbsp; hgkll hgjkf lghjfkd slhgjfd klhgjfds klghjfds s&amp;é@èù</p>
<p>&nbsp;</p>
<p>vghriqghrghgfd&nbsp; hgkll hgjkf lghjfkd slhgjfd klhgjfds klghjfds s&amp;é@èù&nbsp; vghriqghrghgfd&nbsp; hgkll hgjkf lghjfkd slhgjfd klhgjfds klghjfds s&amp;é@èù vghriqghrghgfd&nbsp; hgkll hgjkf lghjfkd slhgjfd klhgjfds klghjfds s&amp;é@èù vghriqghrghgfd&nbsp; hgkll hgjkf lghjfkd slhgjfd klhgjfds klghjfds s&amp;é@èù vghriqghrghgfd&nbsp; hgkll hgjkf lghjfkd slhgjfd klhgjfds klghjfds s&amp;é@èù vghriqghrghgfd&nbsp; hgkll hgjkf lghjfkd slhgjfd klhgjfds klghjfds s&amp;é@èù vghriqghrghgfd&nbsp; hgkll hgjkf lghjfkd slhgjfd klhgjfds klghjfds s&amp;é@</p>
'''

View file

@ -0,0 +1,60 @@
# -*- coding: utf-8 -*-
xhtmlInput = '''
<p>desc 2611-03</p>
<p><br /></p>
<blockquote>
<p>identation 1</p>
<p>identation 2</p>
<p>identation 3</p>
</blockquote>
<p><br /></p>
<ol><li>point numéroté 1</li>
<ol><li>point numéroté 1.1</li><li>point numéroté 1.2</li></ol>
<li>point numéroté 2</li>
<ol><li>point numéroté 2.1</li><li>point numéroté 2.2</li><li>point numéroté 2.3</li>
<ol><li>point numéroté 2.3.1</li><li>point numéroté 2.3.2</li></ol>
</ol>
<li>point numéroté 3</li></ol>
<br />
<ul><li>grosse lune niveau 1</li>
<ul><li>grosse lune niveau 2</li>
<ul><li>grosse lune niveau 3</li>
<ul><li>grosse lune niveau 4<br /></li></ul>
</ul>
</ul>
</ul>
<ul>
<ul>
<ul>
<ul>
<ul><li>grosse lune niveau 5<br /></li>
<ul><li>grosse lune niveau 6<br /></li>
<ul><li>grosse lune niveau 7</li>
<ul><li>grosse lune niveau 8</li>
<ul><li>grosse lune niveau 9</li>
<ul><li>grosse lune niveau 10</li></ul>
</ul>
</ul>
</ul>
</ul>
</ul>
</ul>
</ul>
</ul>
</ul>
<br /><br />
<dl><dt>titre liste 1 </dt><dd>liste 1 </dd><dt>titre liste 2 </dt><dd>liste 2 </dd><dt>titre liste 3 </dt><dd>liste 3 </dd><dt>
<div align="center"><b>texte normal<br /></b></div>
</dt></dl>
<ol type="I"><li>romain maj 1</li><li>romain maj 2</li></ol>
<br />
<ol type="i"><li>romain 1</li><li>romain 2<br /></li></ol>
<dl>
<dl><dt><br /></dt></dl>
</dl>
<ol type="A"><li>alpha maj 1<br /></li><li>alpha maj 2</li></ol>
<br />
<ol type="a"><li>alpha min 1</li><li>alpha min 2</li></ol>
<br />blablabla<br />
<dl><dt><br /></dt></dl>
'''

View file

@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-
xhtmlInput = '<div class="document">\n<p>Hallo?</p>\n</div>\n'

View file

@ -0,0 +1,13 @@
xhtmlInput = '''
<div class="document">
<p>Some <strong>bold</strong> and some <em>italic</em> text.</p>
<p>A new paragraph.</p>
<p>A list with three items:</p>
<ul>
<li>the first item</li>
<li>another item</li>
<li>the last item</li>
</ul>
<p>A last paragraph.</p>
</div>
'''

View file

@ -0,0 +1,9 @@
# -*- coding: utf-8 -*-
xhtmlInput = '''
<ol><li>
<div style="text-align: justify;">Le Gouvernement adopte le projet darrêté modifiant l'arrêté du 9 février 1998 portant délégations de compétence et de signature aux fonctionnaires généraux et à certains autres agents des services du Gouvernement de la Communauté française - Ministère de la Communauté française.</div>
</li><li>
<div style="text-align: justify;">Il charge le Ministre de la Fonction publique de l'exécution de la présente décision.</div>
</li></ol>
<p class="pmParaKeepWithNext">&nbsp;</p>
'''

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,16 @@
# -*- coding: utf-8 -*-
xhtmlInput = '''
<p><meta http-equiv="CONTENT-TYPE" content="text/html; charset=utf-8" /><title></title><meta name="GENERATOR" content="OpenOffice.org 3.0 (Win32)" /><style type="text/css">
&lt;!--
@page { margin: 2cm }
P { margin-bottom: 0.21cm }
--&gt;
</style>
<p>concepteurs de normes : membres des
cabinets ministériels et les administrations.</p>
<p><br /><br /></p>
<p>Laurent, membre du cabinet du Ministre
de l'énergie, doit rédiger un arrêté du Gouvernement wallon
relatif à l'octroi d'une prime à l'isolation. Il peut télécharger
le canevas typ</p>
</p>'''

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

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