moved sources into subdirectory for easier setup
This commit is contained in:
parent
4f91a30fec
commit
d93f8ce937
190 changed files with 4 additions and 4 deletions
125
appy/__init__.py
Normal file
125
appy/__init__.py
Normal 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
89
appy/pod/__init__.py
Normal 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
385
appy/pod/actions.py
Normal 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
733
appy/pod/buffers.py
Normal 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
308
appy/pod/converter.py
Normal 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
423
appy/pod/doc_importers.py
Normal 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
230
appy/pod/elements.py
Normal 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
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
54
appy/pod/odf_parser.py
Normal 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
99
appy/pod/parts.py
Normal 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
382
appy/pod/pod_parser.py
Normal 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
573
appy/pod/renderer.py
Normal 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)
|
||||
# ------------------------------------------------------------------------------
|
147
appy/pod/styles.in.content.xml
Normal file
147
appy/pod/styles.in.content.xml
Normal 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!>
|
18
appy/pod/styles.in.styles.xml
Normal file
18
appy/pod/styles.in.styles.xml
Normal 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
405
appy/pod/styles_manager.py
Normal 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
16
appy/pod/test/Readme.txt
Normal 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
234
appy/pod/test/Tester.py
Normal 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
1895
appy/pod/test/Tests.rtf
Normal file
File diff suppressed because it is too large
Load diff
1
appy/pod/test/__init__.py
Normal file
1
appy/pod/test/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
|
5
appy/pod/test/contexts/Chart1.py
Normal file
5
appy/pod/test/contexts/Chart1.py
Normal file
|
@ -0,0 +1,5 @@
|
|||
johnScore = 25
|
||||
markScore = 53
|
||||
wilsonScore = 12
|
||||
meghuScore = 59
|
||||
|
137
appy/pod/test/contexts/ColgroupTable.py
Normal file
137
appy/pod/test/contexts/ColgroupTable.py
Normal 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>
|
||||
'''
|
10
appy/pod/test/contexts/ElseStatements.py
Normal file
10
appy/pod/test/contexts/ElseStatements.py
Normal file
|
@ -0,0 +1,10 @@
|
|||
trueCondition = True
|
||||
falseCondition = False
|
||||
|
||||
class O:
|
||||
def __init__(self, v):
|
||||
self.v = v
|
||||
self.vv = v+v
|
||||
|
||||
oooo = [O('a'), O('b'), O('c'), O('d')]
|
||||
|
1
appy/pod/test/contexts/Empty.py
Normal file
1
appy/pod/test/contexts/Empty.py
Normal file
|
@ -0,0 +1 @@
|
|||
# This file is really empty.
|
2
appy/pod/test/contexts/FieldExpressions.py
Normal file
2
appy/pod/test/contexts/FieldExpressions.py
Normal file
|
@ -0,0 +1,2 @@
|
|||
old = 'OLD'
|
||||
new = 'NEW'
|
6
appy/pod/test/contexts/FileHandlerImport.py
Normal file
6
appy/pod/test/contexts/FileHandlerImport.py
Normal 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__))
|
||||
|
17
appy/pod/test/contexts/ForCell6.py
Normal file
17
appy/pod/test/contexts/ForCell6.py
Normal 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'),
|
||||
]
|
3
appy/pod/test/contexts/IfAndFors1.py
Normal file
3
appy/pod/test/contexts/IfAndFors1.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from appy.pod.test.contexts import Group
|
||||
|
||||
groups = [Group('group1'), Group('group2'), Group('toto')]
|
6
appy/pod/test/contexts/ImagesImport.py
Normal file
6
appy/pod/test/contexts/ImagesImport.py
Normal file
|
@ -0,0 +1,6 @@
|
|||
import os.path
|
||||
import appy
|
||||
|
||||
def getAppyPath():
|
||||
return os.path.dirname(appy.__file__)
|
||||
|
4
appy/pod/test/contexts/OdsSimple.py
Normal file
4
appy/pod/test/contexts/OdsSimple.py
Normal file
|
@ -0,0 +1,4 @@
|
|||
data = [ \
|
||||
['1', 2, 'three'],
|
||||
['A', 'BB', 'CCC']
|
||||
]
|
3
appy/pod/test/contexts/OnlyExpressions.py
Normal file
3
appy/pod/test/contexts/OnlyExpressions.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
expr1 = 'hello'
|
||||
i1 = 45
|
||||
f1 = 78.05
|
6
appy/pod/test/contexts/PathImport.py
Normal file
6
appy/pod/test/contexts/PathImport.py
Normal file
|
@ -0,0 +1,6 @@
|
|||
import os.path
|
||||
import appy
|
||||
|
||||
def getAppyPath():
|
||||
return os.path.dirname(appy.__file__)
|
||||
|
4
appy/pod/test/contexts/PersonsEight.py
Normal file
4
appy/pod/test/contexts/PersonsEight.py
Normal file
|
@ -0,0 +1,4 @@
|
|||
from appy.pod.test.contexts import Person
|
||||
|
||||
persons = [Person('P1'), Person('P2'), Person('P3'), Person('P4'),
|
||||
Person('P5'), Person('P6'), Person('P7'), Person('P8')]
|
3
appy/pod/test/contexts/PersonsFour.py
Normal file
3
appy/pod/test/contexts/PersonsFour.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from appy.pod.test.contexts import Person
|
||||
|
||||
persons = [Person('P1'), Person('P2'), Person('P3'), Person('P4')]
|
3
appy/pod/test/contexts/PersonsThree.py
Normal file
3
appy/pod/test/contexts/PersonsThree.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from appy.pod.test.contexts import Person
|
||||
|
||||
persons = [Person('P1'), Person('P2'), Person('P3')]
|
3
appy/pod/test/contexts/PersonsTwo.py
Normal file
3
appy/pod/test/contexts/PersonsTwo.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from appy.pod.test.contexts import Person
|
||||
|
||||
persons = [Person('P1'), Person('P2')]
|
1
appy/pod/test/contexts/SimpleForEmptyList.py
Normal file
1
appy/pod/test/contexts/SimpleForEmptyList.py
Normal file
|
@ -0,0 +1 @@
|
|||
list1 = []
|
1
appy/pod/test/contexts/SimpleForFilledList.py
Normal file
1
appy/pod/test/contexts/SimpleForFilledList.py
Normal file
|
@ -0,0 +1 @@
|
|||
list1 = ['Hello', 'World', 45, True]
|
3
appy/pod/test/contexts/SimpleForRow.py
Normal file
3
appy/pod/test/contexts/SimpleForRow.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from appy.pod.test.contexts import Person
|
||||
|
||||
persons = [Person('Mr 1'), Person('Ms One'), Person('Misss two')]
|
1
appy/pod/test/contexts/SimpleIfIsFalse.py
Normal file
1
appy/pod/test/contexts/SimpleIfIsFalse.py
Normal file
|
@ -0,0 +1 @@
|
|||
c1 = False
|
1
appy/pod/test/contexts/SimpleIfIsTrue.py
Normal file
1
appy/pod/test/contexts/SimpleIfIsTrue.py
Normal file
|
@ -0,0 +1 @@
|
|||
c1 = True
|
2
appy/pod/test/contexts/SimpleTest.py
Normal file
2
appy/pod/test/contexts/SimpleTest.py
Normal file
|
@ -0,0 +1,2 @@
|
|||
IWillTellYouWhatInAMoment = 'return'
|
||||
beingPaidForIt = True
|
2
appy/pod/test/contexts/VarStatements.py
Normal file
2
appy/pod/test/contexts/VarStatements.py
Normal file
|
@ -0,0 +1,2 @@
|
|||
var1 = 'VAR1 not overridden'
|
||||
var2 = 'VAR2 not overridden'
|
25
appy/pod/test/contexts/XhtmlComplex.py
Normal file
25
appy/pod/test/contexts/XhtmlComplex.py
Normal file
|
@ -0,0 +1,25 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
xhtmlInput = '''
|
||||
<p>Te<b>s</b>t1 : <b>bold</b>, i<i>tal</i>ics, exponent<sup>34</sup>, sub<sub>45</sub>.</p>
|
||||
<p>An <a href="http://www.google.com">hyperlink</a> to Google.</p>
|
||||
<ol><li>Number list, item 1</li>
|
||||
<ol><li>Sub-item 1</li><li>Sub-Item 2</li>
|
||||
<ol><li>Sub-sub-item A</li><li>Sub-sub-item B <i>italic</i>.</li></ol>
|
||||
</ol>
|
||||
</ol>
|
||||
<ul><li>A bullet</li>
|
||||
<ul><li>A sub-bullet</li>
|
||||
<ul><li>A sub-sub-bullet</li></ul>
|
||||
<ol><li>A sub-sub number</li><li>Another.<br /></li></ol>
|
||||
</ul>
|
||||
</ul>
|
||||
<h2>Heading<br /></h2>
|
||||
Heading Blabla.<br />
|
||||
<h3>SubHeading</h3>
|
||||
Subheading blabla.<br />
|
||||
'''
|
||||
# I need a class.
|
||||
class D:
|
||||
def getAt1(self):
|
||||
return xhtmlInput
|
||||
dummy = D()
|
30
appy/pod/test/contexts/XhtmlComplex2.py
Normal file
30
appy/pod/test/contexts/XhtmlComplex2.py
Normal 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>'''
|
95
appy/pod/test/contexts/XhtmlComplex3.py
Normal file
95
appy/pod/test/contexts/XhtmlComplex3.py
Normal 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 l’affectation du solde de 20.855.107 € du solde des CRAC Ibis et II au sein du secteur des hôpitaux de l’enveloppe 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 à l’octroi 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é d’appliquer, le cas échéant, les nouveaux plafonds à la construction visés dans l’arrêté ministériel du 11 mai 2007 fixant le coût maximal pouvant être pris en considération pour l’octroi des subventions pour la construction de nouveaux bâtiments, les travaux d’extension et de reconditionnement d’un hôpital ou d’un service, aux demandes d’octroi de subventions antérieures au 1<sup>er</sup> janvier 2006, pour autant que ces demandes d’octroi de subventions n’aient pas encore donné lieu à l’exploitation à cette même date.<br /></li>
|
||||
<li>Il charge le Ministre de la Santé de l’exé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>
|
||||
'''
|
44
appy/pod/test/contexts/XhtmlComplex4.py
Normal file
44
appy/pod/test/contexts/XhtmlComplex4.py
Normal 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> </p>
|
||||
<p style="margin-left: 40px;">hghdghghgh</p>
|
||||
<ul>
|
||||
<li>aaa</li>
|
||||
<li> </li>
|
||||
<li>bvbb</li>
|
||||
</ul>
|
||||
<ol>
|
||||
<li>regrg</li>
|
||||
<li> </li>
|
||||
</ol>
|
||||
<p>vghriqghrghgfd hgkll hgjkf lghjfkd slhgjfd klhgjfds klghjfds s&é@èù vghriqghrghgfd hgkll hgjkf lghjfkd slhgjfd klhgjfds klghjfds s&é@èù vghriqghrghgfd hgkll hgjkf lghjfkd slhgjfd klhgjfds klghjfds s&é@èù vghriqghrghgfd hgkll hgjkf lghjfkd slhgjfd klhgjfds klghjfds s&é@èù vghriqghrghgfd hgkll hgjkf lghjfkd slhgjfd klhgjfds klghjfds s&é@èù vghriqghrghgfd hgkll hgjkf lghjfkd slhgjfd klhgjfds klghjfds s&é@èù vghriqghrghgfd hgkll hgjkf lghjfkd slhgjfd klhgjfds klghjfds s&é@èù</p>
|
||||
<p> </p>
|
||||
<p>vghriqghrghgfd hgkll hgjkf lghjfkd slhgjfd klhgjfds klghjfds s&é@èù vghriqghrghgfd hgkll hgjkf lghjfkd slhgjfd klhgjfds klghjfds s&é@èù vghriqghrghgfd hgkll hgjkf lghjfkd slhgjfd klhgjfds klghjfds s&é@èù vghriqghrghgfd hgkll hgjkf lghjfkd slhgjfd klhgjfds klghjfds s&é@èù vghriqghrghgfd hgkll hgjkf lghjfkd slhgjfd klhgjfds klghjfds s&é@èù vghriqghrghgfd hgkll hgjkf lghjfkd slhgjfd klhgjfds klghjfds s&é@èù vghriqghrghgfd hgkll hgjkf lghjfkd slhgjfd klhgjfds klghjfds s&é@</p>
|
||||
'''
|
60
appy/pod/test/contexts/XhtmlComplex5.py
Normal file
60
appy/pod/test/contexts/XhtmlComplex5.py
Normal 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>
|
||||
'''
|
2
appy/pod/test/contexts/XhtmlComplex6.py
Normal file
2
appy/pod/test/contexts/XhtmlComplex6.py
Normal file
|
@ -0,0 +1,2 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
xhtmlInput = '<div class="document">\n<p>Hallo?</p>\n</div>\n'
|
13
appy/pod/test/contexts/XhtmlComplex7.py
Normal file
13
appy/pod/test/contexts/XhtmlComplex7.py
Normal 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>
|
||||
'''
|
9
appy/pod/test/contexts/XhtmlComplex8.py
Normal file
9
appy/pod/test/contexts/XhtmlComplex8.py
Normal file
|
@ -0,0 +1,9 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
xhtmlInput = '''
|
||||
<ol><li>
|
||||
<div style="text-align: justify;">Le Gouvernement adopte le projet d’arrê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"> </p>
|
||||
'''
|
57
appy/pod/test/contexts/XhtmlComplexTables.py
Normal file
57
appy/pod/test/contexts/XhtmlComplexTables.py
Normal 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>
|
||||
'''
|
5
appy/pod/test/contexts/XhtmlEntities.py
Normal file
5
appy/pod/test/contexts/XhtmlEntities.py
Normal file
|
@ -0,0 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
xhtmlInput = '''
|
||||
<p>Some HTML entities: é: é, è: è, Atilde: Ã.</p>
|
||||
<p>XML entities: amp: &, quote: ", apos: ', lt: <, gt: >.</p>
|
||||
<p> </p><p>Para</p>'''
|
44
appy/pod/test/contexts/XhtmlKeepWithNext.py
Normal file
44
appy/pod/test/contexts/XhtmlKeepWithNext.py
Normal file
|
@ -0,0 +1,44 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# I need a class.
|
||||
class D:
|
||||
def getAt1(self):
|
||||
return '''
|
||||
<p>Notifia</p>
|
||||
<ol>
|
||||
<li>Een</li>
|
||||
<li>Een</li>
|
||||
<li>Een</li>
|
||||
<li class="podItemKeepWithNext">Keep with next, without style mapping.</li>
|
||||
<li>Een</li>
|
||||
<li>Een</li>
|
||||
<li>Een</li>
|
||||
<li>Een</li>
|
||||
<li>Een</li>
|
||||
<li>Een</li>
|
||||
<li>Een</li>
|
||||
<ul><li class="pmItemKeepWithNext">This one has 'keep with next'</li>
|
||||
<li>Hello</li>
|
||||
<ol><li>aaaaaaaaaa aaaaaaaaaaaaaa</li>
|
||||
<li>aaaaaaaaaa aaaaaaaaaaaaaa</li>
|
||||
<li>aaaaaaaaaa aaaaaaaaaaaaaa</li>
|
||||
<li class="pmItemKeepWithNext">This one has 'keep with next'</li>
|
||||
</ol>
|
||||
</ul>
|
||||
<li>Een</li>
|
||||
<li>Een</li>
|
||||
<li>Een</li>
|
||||
<li>Een</li>
|
||||
<li>Een</li>
|
||||
<li>Een</li>
|
||||
<li class="pmItemKeepWithNext">This one has 'keep with next'</li>
|
||||
</ol>
|
||||
<ul>
|
||||
<li>Un</li>
|
||||
<li>Deux</li>
|
||||
<li>Trois</li>
|
||||
<li>Quatre</li>
|
||||
<li class="pmItemKeepWithNext">VCinq (this one has 'keep with next')</li>
|
||||
<li class="pmItemKeepWithNext">Six (this one has 'keep with next')</li>
|
||||
<li class="pmItemKeepWithNext">Sept (this one has 'keep with next')</li>
|
||||
</ul>'''
|
||||
dummy = D()
|
6
appy/pod/test/contexts/XhtmlNominal.py
Normal file
6
appy/pod/test/contexts/XhtmlNominal.py
Normal file
|
@ -0,0 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# I need a class.
|
||||
class D:
|
||||
def getAt1(self):
|
||||
return '\n<p>Test1<br /></p>\n'
|
||||
dummy = D()
|
12
appy/pod/test/contexts/XhtmlStylesErrors.py
Normal file
12
appy/pod/test/contexts/XhtmlStylesErrors.py
Normal file
|
@ -0,0 +1,12 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
xhtmlInput = '''
|
||||
<p>Hello.</p>
|
||||
<h2>Heading One</h2>
|
||||
Blabla.<br />
|
||||
<h3>SubHeading then.</h3>
|
||||
Another blabla.<br /><br /><br /> '''
|
||||
# I need a class.
|
||||
class D:
|
||||
def getAt1(self):
|
||||
return xhtmlInput
|
||||
dummy = D()
|
12
appy/pod/test/contexts/XhtmlStylesMapping.py
Normal file
12
appy/pod/test/contexts/XhtmlStylesMapping.py
Normal file
|
@ -0,0 +1,12 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
xhtmlInput = '''
|
||||
<p>Hello.</p>
|
||||
<h2>Heading One</h2>
|
||||
Blabla.<br />
|
||||
<h3>SubHeading then.</h3>
|
||||
Another blabla.<br /><br /><br /> '''
|
||||
# I need a class.
|
||||
class D:
|
||||
def getAt1(self):
|
||||
return xhtmlInput
|
||||
dummy = D()
|
35
appy/pod/test/contexts/XhtmlTables.py
Normal file
35
appy/pod/test/contexts/XhtmlTables.py
Normal 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 />'''
|
16
appy/pod/test/contexts/XhtmlWithStyle.py
Normal file
16
appy/pod/test/contexts/XhtmlWithStyle.py
Normal 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">
|
||||
<!--
|
||||
@page { margin: 2cm }
|
||||
P { margin-bottom: 0.21cm }
|
||||
-->
|
||||
</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>'''
|
18
appy/pod/test/contexts/__init__.py
Normal file
18
appy/pod/test/contexts/__init__.py
Normal file
|
@ -0,0 +1,18 @@
|
|||
# Here I define some classes that will be used for defining objects in several
|
||||
# contexts.
|
||||
class Person:
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
self.lastName = '%s last name' % name
|
||||
self.firstName = '%s first name' % name
|
||||
self.address = '%s address' % name
|
||||
|
||||
class Group:
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
if name == 'group1':
|
||||
self.persons = [Person('P1'), Person('P2'), Person('P3')]
|
||||
elif name == 'group2':
|
||||
self.persons = [Person('RA'), Person('RB')]
|
||||
else:
|
||||
self.persons = []
|
BIN
appy/pod/test/images/linux.jpg
Normal file
BIN
appy/pod/test/images/linux.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.5 KiB |
BIN
appy/pod/test/images/plone.png
Normal file
BIN
appy/pod/test/images/plone.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.4 KiB |
BIN
appy/pod/test/images/python.gif
Normal file
BIN
appy/pod/test/images/python.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.2 KiB |
BIN
appy/pod/test/results/chart1.odt
Normal file
BIN
appy/pod/test/results/chart1.odt
Normal file
Binary file not shown.
BIN
appy/pod/test/results/elseStatements.odt
Normal file
BIN
appy/pod/test/results/elseStatements.odt
Normal file
Binary file not shown.
BIN
appy/pod/test/results/errorExpression.odt
Normal file
BIN
appy/pod/test/results/errorExpression.odt
Normal file
Binary file not shown.
BIN
appy/pod/test/results/errorFooter.odt
Normal file
BIN
appy/pod/test/results/errorFooter.odt
Normal file
Binary file not shown.
BIN
appy/pod/test/results/errorForParsetime.odt
Normal file
BIN
appy/pod/test/results/errorForParsetime.odt
Normal file
Binary file not shown.
BIN
appy/pod/test/results/errorForRuntime.odt
Normal file
BIN
appy/pod/test/results/errorForRuntime.odt
Normal file
Binary file not shown.
BIN
appy/pod/test/results/errorIf.odt
Normal file
BIN
appy/pod/test/results/errorIf.odt
Normal file
Binary file not shown.
BIN
appy/pod/test/results/fieldExpression.odt
Normal file
BIN
appy/pod/test/results/fieldExpression.odt
Normal file
Binary file not shown.
BIN
appy/pod/test/results/fileHandlerImport.odt
Normal file
BIN
appy/pod/test/results/fileHandlerImport.odt
Normal file
Binary file not shown.
BIN
appy/pod/test/results/forCellBug.odt
Normal file
BIN
appy/pod/test/results/forCellBug.odt
Normal file
Binary file not shown.
BIN
appy/pod/test/results/forCellBug2.odt
Normal file
BIN
appy/pod/test/results/forCellBug2.odt
Normal file
Binary file not shown.
BIN
appy/pod/test/results/forCellCorrectNumber.odt
Normal file
BIN
appy/pod/test/results/forCellCorrectNumber.odt
Normal file
Binary file not shown.
BIN
appy/pod/test/results/forCellNotEnough.odt
Normal file
BIN
appy/pod/test/results/forCellNotEnough.odt
Normal file
Binary file not shown.
BIN
appy/pod/test/results/forCellOnlyOne.odt
Normal file
BIN
appy/pod/test/results/forCellOnlyOne.odt
Normal file
Binary file not shown.
BIN
appy/pod/test/results/forCellTooMuch1.odt
Normal file
BIN
appy/pod/test/results/forCellTooMuch1.odt
Normal file
Binary file not shown.
BIN
appy/pod/test/results/forCellTooMuch2.odt
Normal file
BIN
appy/pod/test/results/forCellTooMuch2.odt
Normal file
Binary file not shown.
BIN
appy/pod/test/results/forCellTooMuch3.odt
Normal file
BIN
appy/pod/test/results/forCellTooMuch3.odt
Normal file
Binary file not shown.
BIN
appy/pod/test/results/forCellTooMuch4.odt
Normal file
BIN
appy/pod/test/results/forCellTooMuch4.odt
Normal file
Binary file not shown.
BIN
appy/pod/test/results/forTable.odt
Normal file
BIN
appy/pod/test/results/forTable.odt
Normal file
Binary file not shown.
BIN
appy/pod/test/results/forTableMinus.odt
Normal file
BIN
appy/pod/test/results/forTableMinus.odt
Normal file
Binary file not shown.
BIN
appy/pod/test/results/forTableMinus2.odt
Normal file
BIN
appy/pod/test/results/forTableMinus2.odt
Normal file
Binary file not shown.
BIN
appy/pod/test/results/forTableMinusError.odt
Normal file
BIN
appy/pod/test/results/forTableMinusError.odt
Normal file
Binary file not shown.
BIN
appy/pod/test/results/forTableMinusError2.odt
Normal file
BIN
appy/pod/test/results/forTableMinusError2.odt
Normal file
Binary file not shown.
BIN
appy/pod/test/results/headerFooter.odt
Normal file
BIN
appy/pod/test/results/headerFooter.odt
Normal file
Binary file not shown.
BIN
appy/pod/test/results/ifAndFors1.odt
Normal file
BIN
appy/pod/test/results/ifAndFors1.odt
Normal file
Binary file not shown.
BIN
appy/pod/test/results/ifElseErrors.odt
Normal file
BIN
appy/pod/test/results/ifElseErrors.odt
Normal file
Binary file not shown.
BIN
appy/pod/test/results/imagesImport.odt
Normal file
BIN
appy/pod/test/results/imagesImport.odt
Normal file
Binary file not shown.
BIN
appy/pod/test/results/noPython.odt
Normal file
BIN
appy/pod/test/results/noPython.odt
Normal file
Binary file not shown.
BIN
appy/pod/test/results/odsSimple.ods
Normal file
BIN
appy/pod/test/results/odsSimple.ods
Normal file
Binary file not shown.
BIN
appy/pod/test/results/onlyExpressions.odt
Normal file
BIN
appy/pod/test/results/onlyExpressions.odt
Normal file
Binary file not shown.
BIN
appy/pod/test/results/pathImport.odt
Normal file
BIN
appy/pod/test/results/pathImport.odt
Normal file
Binary file not shown.
BIN
appy/pod/test/results/simpleForEmptyList.odt
Normal file
BIN
appy/pod/test/results/simpleForEmptyList.odt
Normal file
Binary file not shown.
BIN
appy/pod/test/results/simpleForFilledList.odt
Normal file
BIN
appy/pod/test/results/simpleForFilledList.odt
Normal file
Binary file not shown.
BIN
appy/pod/test/results/simpleForRow.odt
Normal file
BIN
appy/pod/test/results/simpleForRow.odt
Normal file
Binary file not shown.
BIN
appy/pod/test/results/simpleFromTest.odt
Normal file
BIN
appy/pod/test/results/simpleFromTest.odt
Normal file
Binary file not shown.
BIN
appy/pod/test/results/simpleIfIsFalse.odt
Normal file
BIN
appy/pod/test/results/simpleIfIsFalse.odt
Normal file
Binary file not shown.
BIN
appy/pod/test/results/simpleIfIsTrue.odt
Normal file
BIN
appy/pod/test/results/simpleIfIsTrue.odt
Normal file
Binary file not shown.
BIN
appy/pod/test/results/simpleIfIsTrue003.odt
Normal file
BIN
appy/pod/test/results/simpleIfIsTrue003.odt
Normal file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue