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