appy.gen: improved cleaning and formatting of XHTML content; appy.pod: added some default appy-related table styles for producing cells with text in bold/normal, aligned right/left, etc.

This commit is contained in:
Gaetan Delannay 2012-05-14 17:35:34 +02:00
parent d3a2b85a10
commit 028040351c
11 changed files with 195 additions and 54 deletions

View file

@ -11,8 +11,9 @@ from appy.gen.utils import GroupDescr, Keywords, getClassName, SomeObjects
import appy.pod import appy.pod
from appy.pod.renderer import Renderer from appy.pod.renderer import Renderer
from appy.shared.data import countries from appy.shared.data import countries
from appy.shared.xml_parser import XhtmlCleaner
from appy.shared.utils import Traceback, getOsTempFolder, formatNumber, \ from appy.shared.utils import Traceback, getOsTempFolder, formatNumber, \
XhtmlCleaner, FileWrapper, sequenceTypes FileWrapper, sequenceTypes
# Default Appy permissions ----------------------------------------------------- # Default Appy permissions -----------------------------------------------------
r, w, d = ('read', 'write', 'delete') r, w, d = ('read', 'write', 'delete')
@ -1238,8 +1239,7 @@ class String(Type):
# When image upload is allowed, ckeditor inserts some "style" attrs # When image upload is allowed, ckeditor inserts some "style" attrs
# (ie for image size when images are resized). So in this case we # (ie for image size when images are resized). So in this case we
# can't remove style-related information. # can't remove style-related information.
keepStyles = self.allowImageUpload or self.richText value = XhtmlCleaner().clean(value, keepStyles=self.richText)
value = XhtmlCleaner.clean(value, keepStyles=keepStyles)
Type.store(self, obj, value) Type.store(self, obj, value)
def getFormattedValue(self, obj, value): def getFormattedValue(self, obj, value):

View file

@ -1,10 +1,14 @@
body { font: 75% Helvetica,Arial,sans-serif; background-color: #EAEAEA; body { font: 75% Helvetica,Arial,sans-serif; background-color: #EAEAEA;
margin-top: 18px} margin-top: 18px}
pre { font: 100% Helvetica,Arial,sans-serif; margin: 0} pre { font: 100% Helvetica,Arial,sans-serif; margin: 0}
h1 { font-size: 11pt; margin:0;} h1 { font-size: 14pt; margin:0;}
h2 { font-size: 10pt; margin:0; font-style: italic; font-weight: normal; h2 { font-size: 13pt; margin:0; font-style: italic; font-weight: normal;
background-color: #d7dee4} background-color: #d7dee4}
h3 { font-size: 9pt; margin:0; font-weight: bold;} h3 { font-size: 12pt; margin:0; font-weight: bold;}
h4 { font-size: 11pt; margin:0;}
h5 { font-size: 10pt; margin:0; font-style: italic; font-weight: normal;
background-color: #d7dee4}
h6 { font-size: 9pt; margin:0; font-weight: bold;}
a { text-decoration: none; color: #503737;} a { text-decoration: none; color: #503737;}
a:visited { color: #503737;} a:visited { color: #503737;}
table { font-size: 100%; border-spacing: 0px; border-collapse:collapse;} table { font-size: 100%; border-spacing: 0px; border-collapse:collapse;}
@ -34,10 +38,15 @@ label { font-weight: 600; font-style: italic; line-height: 1.4em;}
legend { padding-bottom: 2px; padding-right: 3px; color: black;} legend { padding-bottom: 2px; padding-right: 3px; color: black;}
ul { line-height: 1.2em; margin: 0 0 0.2em 0.6em; padding: 0; ul { line-height: 1.2em; margin: 0 0 0.2em 0.6em; padding: 0;
list-style: none outside none;} list-style: none outside none;}
li { margin: 0; background-image: url("ui/li.gif"); padding-left: 10px; ul li { margin: 0; background-image: url("ui/li.gif"); padding-left: 10px;
background-repeat: no-repeat; background-position: 0 4px;} background-repeat: no-repeat; background-position: 0 4px;}
img {border: 0} img {border: 0}
/* Styles that apply when viewing content of XHTML fields, that mimic styles
that ckeditor uses for displaying XHTML content in the edit view. */
.xhtml { margin-top: 10px }
.xhtml img { margin-right: 5px } .xhtml img { margin-right: 5px }
.xhtml p { margin: 3px 0 7px 0}
.main { width: 900px; background-color: white; box-shadow: 3px 3px 3px #A9A9A9; .main { width: 900px; background-color: white; box-shadow: 3px 3px 3px #A9A9A9;
border-style: solid; border-width: 1px; border-color: grey} border-style: solid; border-width: 1px; border-color: grey}

View file

@ -30,4 +30,8 @@ CKEDITOR.editorConfig = function( config )
config.format_h2 = { element:'h2', attributes:{'style':'margin:0;padding:0'}}; config.format_h2 = { element:'h2', attributes:{'style':'margin:0;padding:0'}};
config.format_h3 = { element:'h3', attributes:{'style':'margin:0;padding:0'}}; config.format_h3 = { element:'h3', attributes:{'style':'margin:0;padding:0'}};
config.format_h4 = { element:'h4', attributes:{'style':'margin:0;padding:0'}}; config.format_h4 = { element:'h4', attributes:{'style':'margin:0;padding:0'}};
config.entities = false;
config.entities_greek = false;
config.entities_latin = false;
config.fillEmptyBlocks = false;
}; };

View file

@ -12,3 +12,4 @@ ol,ul,dl {
padding:0 40px; padding:0 40px;
} }
img { margin-right: 5px} img { margin-right: 5px}
table { border-collapse: collapse; border-spacing: 0 }

View file

@ -149,7 +149,7 @@ class ToolWrapper(AbstractWrapper):
'''Reindex all Appy objects. For some unknown reason, method '''Reindex all Appy objects. For some unknown reason, method
catalog.refreshCatalog is not able to recatalog Appy objects.''' catalog.refreshCatalog is not able to recatalog Appy objects.'''
if not startObject: if not startObject:
# This is a global refresh. Clear the catallog completely, and then # This is a global refresh. Clear the catalog completely, and then
# reindex all Appy-managed objects, ie those in folders "config" # reindex all Appy-managed objects, ie those in folders "config"
# and "data". # and "data".
# First, clear the catalog. # First, clear the catalog.

View file

@ -78,7 +78,7 @@ CONTENT_POD_STYLES = f.read()
f.close() f.close()
# Default font added by pod in content.xml # Default font added by pod in content.xml
CONTENT_POD_FONTS = '<@style@:font-face style:name="PodStarSymbol" ' \ CONTENT_POD_FONTS = '<@style@:font-face @style@:name="PodStarSymbol" ' \
'@svg@:font-family="StarSymbol"/>' '@svg@:font-family="StarSymbol"/>'
# Default text styles added by pod in styles.xml # Default text styles added by pod in styles.xml
@ -213,7 +213,8 @@ class Renderer:
nsUris={'style': pe.NS_STYLE, 'svg': pe.NS_SVG}), nsUris={'style': pe.NS_STYLE, 'svg': pe.NS_SVG}),
OdInsert(STYLES_POD_STYLES, OdInsert(STYLES_POD_STYLES,
XmlElement('styles', nsUri=pe.NS_OFFICE), XmlElement('styles', nsUri=pe.NS_OFFICE),
nsUris={'style': pe.NS_STYLE, 'fo': pe.NS_FO})) nsUris={'style': pe.NS_STYLE, 'fo': pe.NS_FO,
'text': pe.NS_TEXT}))
self.stylesParser = self.createPodParser('styles.xml', context, self.stylesParser = self.createPodParser('styles.xml', context,
stylesInserts) stylesInserts)
# Stores the styles mapping # Stores the styles mapping

View file

@ -119,3 +119,25 @@
<@style@:style @style@:name="podImageRight" @style@:family="graphic" @style@:parent-style-name="Graphics"> <@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@: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@: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>

View file

@ -4,3 +4,18 @@
@style@:font-name-asian="PodStarSymbol" @style@:font-size-asian="9pt" @style@:font-name-asian="PodStarSymbol" @style@:font-size-asian="9pt"
@style@:font-name-complex="PodStarSymbol" @style@:font-size-complex="9pt"/> @style@:font-name-complex="PodStarSymbol" @style@:font-size-complex="9pt"/>
</@style@:style> </@style@:style>
<@style@:style style:name="AppyStandard" style:family="paragraph" style:class="text" style:master-page-name="">
<@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@:text-properties style:font-name="DejaVu Sans" fo:font-size="10pt"/>
</@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@:text-properties @fo@:font-size="8pt"/>
</@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>

View file

@ -106,7 +106,8 @@ class Debianizer:
def __init__(self, app, out, appVersion='0.1.0', def __init__(self, app, out, appVersion='0.1.0',
pythonVersions=('2.6',), zopePort=8080, pythonVersions=('2.6',), zopePort=8080,
depends=('openoffice.org', 'imagemagick'), sign=False): depends=('zope2.12', 'openoffice.org', 'imagemagick'),
sign=False):
# app is the path to the Python package to Debianize. # app is the path to the Python package to Debianize.
self.app = app self.app = app
self.appName = os.path.basename(app) self.appName = os.path.basename(app)
@ -261,10 +262,6 @@ class Debianizer:
# Create postinst, a script that will: # Create postinst, a script that will:
# - bytecompile Python files after the Debian install # - bytecompile Python files after the Debian install
# - change ownership of some files if required # - change ownership of some files if required
# - [in the case of a app-package] execute:
# apt-get -t squeeze-backports install zope2.12
# (if zope2.12 is defined as a simple dependency in field "Depends:"
# it will fail because it will not be searched in squeeze-backports).
# - [in the case of an app-package] call update-rc.d for starting it at # - [in the case of an app-package] call update-rc.d for starting it at
# boot time. # boot time.
f = file('postinst', 'w') f = file('postinst', 'w')
@ -276,8 +273,6 @@ class Debianizer:
self.appName) self.appName)
content += 'if [ -e %s ]\nthen\n%sfi\n' % (bin, cmds) content += 'if [ -e %s ]\nthen\n%sfi\n' % (bin, cmds)
if self.appName != 'appy': if self.appName != 'appy':
# Install zope2.12 from squeeze-backports
content += 'apt-get -t squeeze-backports install zope2.12\n'
# Allow user "zope", that runs the Zope instance, to write the # Allow user "zope", that runs the Zope instance, to write the
# database and log files. # database and log files.
content += 'chown -R zope:root /var/lib/%s\n' % self.appNameLower content += 'chown -R zope:root /var/lib/%s\n' % self.appNameLower

View file

@ -263,35 +263,6 @@ def formatNumber(n, sep=',', precision=2, tsep=' '):
res += sep + splitted[1] res += sep + splitted[1]
return res return res
# ------------------------------------------------------------------------------
class XhtmlCleaner:
# Regular expressions used for cleaning.
classAttr = re.compile('class\s*=\s*".*?"')
comment = re.compile('<!--.*?-->', re.S)
'''This class has 2 objectives:
1. The main objective is to format XHTML p_s to be storable in the ZODB
according to Appy rules.
a. Every <p> or <li> must be on a single line (ending with a carriage
return); else, appy.shared.diff will not be able to compute XHTML
diffs;
b. Optimize size: HTML comments are removed.
2. If p_keepStyles (or m_clean) is False, some style-related information
will be removed, in order to get a standardized content that can be
dumped in an elegant and systematic manner into a POD template.
'''
@classmethod
def clean(klass, s, keepStyles=False):
'''Returns the cleaned variant of p_s.'''
if not keepStyles:
# Format p_s according to objective 2.
s = klass.classAttr.sub('', s)
# Format p_s according to objective 1.
s = klass.comment.sub('', s)
return s
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
def lower(s): def lower(s):
'''French-accents-aware variant of string.lower.''' '''French-accents-aware variant of string.lower.'''

View file

@ -18,7 +18,7 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,USA.
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
import xml.sax, difflib, types import xml.sax, difflib, types, cgi
from xml.sax.handler import ContentHandler, ErrorHandler, feature_external_ges,\ from xml.sax.handler import ContentHandler, ErrorHandler, feature_external_ges,\
property_interning_dict property_interning_dict
from xml.sax.xmlreader import InputSource from xml.sax.xmlreader import InputSource
@ -887,4 +887,127 @@ class XmlComparator:
else: else:
lastLinePrinted = False lastLinePrinted = False
return not atLeastOneDiff return not atLeastOneDiff
# ------------------------------------------------------------------------------
class XhtmlCleaner(XmlParser):
# Tags that will not be in the result, content included, if keepStyles is
# False.
tagsToIgnoreWithContent = ('style', 'colgroup')
# Tags that will be removed from the result, but whose content will be kept,
# if keepStyles is False.
tagsToIgnoreKeepContent= ('x', 'font')
# All tags to ignore
tagsToIgnore = tagsToIgnoreWithContent + tagsToIgnoreKeepContent
# Attributes to ignore, if keepStyles if False.
attrsToIgnore = ('align', 'valign', 'cellpadding', 'cellspacing', 'width',
'height', 'bgcolor', 'lang', 'border', 'class')
# Attrs to add, if not present, to ensure good formatting, be it at the web
# or ODT levels.
attrsToAdd = {'table': {'cellspacing':'0', 'cellpadding':'6', 'border':'1'},
'tr': {'valign': 'top'}}
# Tags that required a line break to be inserted after them.
lineBreakTags = ('p', 'li', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'td')
'''This class has 2 objectives:
1. The main objective is to format XHTML p_s to be storable in the ZODB
according to Appy rules.
a. Every <p> or <li> must be on a single line (ending with a carriage
return); else, appy.shared.diff will not be able to compute XHTML
diffs;
b. Optimize size: HTML comments are removed.
2. If p_keepStyles (or m_clean) is False, some style-related information
will be removed, in order to get a standardized content that can be
dumped in an elegant and systematic manner into a POD template.
'''
def clean(self, s, keepStyles=True):
# Must we keep style-related information or not?
self.env.keepStyles = keepStyles
self.env.currentContent = ''
# The stack of currently parsed elements (will contain only ignored
# ones).
self.env.currentElems = []
# 'ignoreTag' is True if we must ignore the currently walked tag.
self.env.ignoreTag = False
# 'ignoreContent' is True if, within the currently ignored tag, we must
# also ignore its content.
self.env.ignoreContent = False
return self.parse('<x>%s</x>' % s)
def startDocument(self):
# The result will be cleaned XHTML, joined from self.res.
self.res = []
def endDocument(self):
self.res = ''.join(self.res)
def startElement(self, elem, attrs):
e = self.env
# Dump any previously gathered content if any
if e.currentContent:
self.res.append(e.currentContent)
e.currentContent = ''
if e.ignoreTag and e.ignoreContent: return
if not e.keepStyles and (elem in self.tagsToIgnore):
e.ignoreTag = True
if elem in self.tagsToIgnoreWithContent:
e.ignoreContent = True
else:
e.ignoreContent = False
e.currentElems.append( (elem, e.ignoreContent) )
return
# Add a line break before the start tag if required (ie: xhtml differ
# needs to get paragraphs and other elements on separate lines).
if (elem in self.lineBreakTags) and self.res and \
(self.res[-1][-1] != '\n'):
prefix = '\n'
else:
prefix = ''
res = '%s<%s' % (prefix, elem)
# Include the found attributes, excepted those that must be ignored.
for name, value in attrs.items():
if not e.keepStyles and (name in self.attrsToIgnore): continue
res += ' %s="%s"' % (name, value)
# Include additional attributes if required.
if elem in self.attrsToAdd:
for name, value in self.attrsToAdd[elem].iteritems():
res += ' %s="%s"' % (name, value)
self.res.append('%s>' % res)
def endElement(self, elem):
e = self.env
if e.ignoreTag and (elem in self.tagsToIgnore):
# Pop the currently ignored tag
e.currentElems.pop()
if e.currentElems:
# Keep ignoring tags.
e.ignoreContent = e.currentElems[-1][1]
else:
# Stop ignoring elems
e.ignoreTag = e.ignoreContent = False
elif e.ignoreTag and e.ignoreContent:
# This is the end of a sub-tag within a region that we must ignore.
pass
else:
self.res.append(self.env.currentContent)
# Add a line break after the end tag if required (ie: xhtml differ
# needs to get paragraphs and other elements on separate lines).
if elem in self.lineBreakTags:
suffix = '\n'
else:
suffix = ''
self.res.append('</%s>%s' % (elem, suffix))
self.env.currentContent = ''
def characters(self, content):
if self.env.ignoreContent: return
# Remove blanks that ckeditor may add just after a start tag
if not self.env.currentContent or (self.env.currentContent == ' '):
toAdd = ' ' + content.lstrip()
else:
toAdd = content
# Re-transform XML special chars to entities.
self.env.currentContent += cgi.escape(content)
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------