From 028040351cdfcfa921c0211b2ebdd6717f69478d Mon Sep 17 00:00:00 2001 From: Gaetan Delannay Date: Mon, 14 May 2012 17:35:34 +0200 Subject: [PATCH] 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. --- gen/__init__.py | 6 +- gen/ui/appy.css | 19 ++++-- gen/ui/ckeditor/config.js | 4 ++ gen/ui/ckeditor/contents.css | 1 + gen/wrappers/ToolWrapper.py | 2 +- pod/renderer.py | 5 +- pod/styles.in.content.xml | 26 +++++++- pod/styles.in.styles.xml | 23 +++++-- shared/packaging.py | 9 +-- shared/utils.py | 29 -------- shared/xml_parser.py | 125 ++++++++++++++++++++++++++++++++++- 11 files changed, 195 insertions(+), 54 deletions(-) diff --git a/gen/__init__.py b/gen/__init__.py index a9441f8..6a480e2 100644 --- a/gen/__init__.py +++ b/gen/__init__.py @@ -11,8 +11,9 @@ from appy.gen.utils import GroupDescr, Keywords, getClassName, SomeObjects import appy.pod from appy.pod.renderer import Renderer from appy.shared.data import countries +from appy.shared.xml_parser import XhtmlCleaner from appy.shared.utils import Traceback, getOsTempFolder, formatNumber, \ - XhtmlCleaner, FileWrapper, sequenceTypes + FileWrapper, sequenceTypes # Default Appy permissions ----------------------------------------------------- r, w, d = ('read', 'write', 'delete') @@ -1238,8 +1239,7 @@ class String(Type): # When image upload is allowed, ckeditor inserts some "style" attrs # (ie for image size when images are resized). So in this case we # can't remove style-related information. - keepStyles = self.allowImageUpload or self.richText - value = XhtmlCleaner.clean(value, keepStyles=keepStyles) + value = XhtmlCleaner().clean(value, keepStyles=self.richText) Type.store(self, obj, value) def getFormattedValue(self, obj, value): diff --git a/gen/ui/appy.css b/gen/ui/appy.css index fd01f47..499d4e9 100644 --- a/gen/ui/appy.css +++ b/gen/ui/appy.css @@ -1,10 +1,14 @@ body { font: 75% Helvetica,Arial,sans-serif; background-color: #EAEAEA; margin-top: 18px} pre { font: 100% Helvetica,Arial,sans-serif; margin: 0} -h1 { font-size: 11pt; margin:0;} -h2 { font-size: 10pt; margin:0; font-style: italic; font-weight: normal; +h1 { font-size: 14pt; margin:0;} +h2 { font-size: 13pt; margin:0; font-style: italic; font-weight: normal; 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:visited { color: #503737;} 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;} ul { line-height: 1.2em; margin: 0 0 0.2em 0.6em; padding: 0; list-style: none outside none;} -li { margin: 0; background-image: url("ui/li.gif"); padding-left: 10px; - background-repeat: no-repeat; background-position: 0 4px;} +ul li { margin: 0; background-image: url("ui/li.gif"); padding-left: 10px; + background-repeat: no-repeat; background-position: 0 4px;} 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 p { margin: 3px 0 7px 0} .main { width: 900px; background-color: white; box-shadow: 3px 3px 3px #A9A9A9; border-style: solid; border-width: 1px; border-color: grey} diff --git a/gen/ui/ckeditor/config.js b/gen/ui/ckeditor/config.js index 3d7e470..2f310bf 100644 --- a/gen/ui/ckeditor/config.js +++ b/gen/ui/ckeditor/config.js @@ -30,4 +30,8 @@ CKEDITOR.editorConfig = function( config ) config.format_h2 = { element:'h2', 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.entities = false; + config.entities_greek = false; + config.entities_latin = false; + config.fillEmptyBlocks = false; }; diff --git a/gen/ui/ckeditor/contents.css b/gen/ui/ckeditor/contents.css index ff10393..a3674d3 100644 --- a/gen/ui/ckeditor/contents.css +++ b/gen/ui/ckeditor/contents.css @@ -12,3 +12,4 @@ ol,ul,dl { padding:0 40px; } img { margin-right: 5px} +table { border-collapse: collapse; border-spacing: 0 } diff --git a/gen/wrappers/ToolWrapper.py b/gen/wrappers/ToolWrapper.py index c6eab9f..c1c7411 100644 --- a/gen/wrappers/ToolWrapper.py +++ b/gen/wrappers/ToolWrapper.py @@ -149,7 +149,7 @@ class ToolWrapper(AbstractWrapper): '''Reindex all Appy objects. For some unknown reason, method catalog.refreshCatalog is not able to recatalog Appy objects.''' 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" # and "data". # First, clear the catalog. diff --git a/pod/renderer.py b/pod/renderer.py index 40e7858..5c646ec 100644 --- a/pod/renderer.py +++ b/pod/renderer.py @@ -78,7 +78,7 @@ CONTENT_POD_STYLES = f.read() f.close() # 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"/>' # Default text styles added by pod in styles.xml @@ -213,7 +213,8 @@ class Renderer: 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})) + nsUris={'style': pe.NS_STYLE, 'fo': pe.NS_FO, + 'text': pe.NS_TEXT})) self.stylesParser = self.createPodParser('styles.xml', context, stylesInserts) # Stores the styles mapping diff --git a/pod/styles.in.content.xml b/pod/styles.in.content.xml index 54d071a..15d9aff 100644 --- a/pod/styles.in.content.xml +++ b/pod/styles.in.content.xml @@ -114,8 +114,30 @@ <@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@: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@: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@: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@: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@: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@: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@:name="podTableCell" @style@:family="table-cell"> + <@style@:table-cell-properties @fo@:padding="0.097cm" @fo@:border="0.018cm solid #000000"/> + +<@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/> + diff --git a/pod/styles.in.styles.xml b/pod/styles.in.styles.xml index c9ff80b..c9e799f 100644 --- a/pod/styles.in.styles.xml +++ b/pod/styles.in.styles.xml @@ -1,6 +1,21 @@ <@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"/> - \ No newline at end of file + <@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: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@: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@: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"/> + diff --git a/shared/packaging.py b/shared/packaging.py index d951b44..53c5b34 100644 --- a/shared/packaging.py +++ b/shared/packaging.py @@ -106,7 +106,8 @@ class Debianizer: def __init__(self, app, out, appVersion='0.1.0', 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. self.app = app self.appName = os.path.basename(app) @@ -261,10 +262,6 @@ class Debianizer: # Create postinst, a script that will: # - bytecompile Python files after the Debian install # - 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 # boot time. f = file('postinst', 'w') @@ -276,8 +273,6 @@ class Debianizer: self.appName) content += 'if [ -e %s ]\nthen\n%sfi\n' % (bin, cmds) 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 # database and log files. content += 'chown -R zope:root /var/lib/%s\n' % self.appNameLower diff --git a/shared/utils.py b/shared/utils.py index 914e4d7..0701c9f 100644 --- a/shared/utils.py +++ b/shared/utils.py @@ -263,35 +263,6 @@ def formatNumber(n, sep=',', precision=2, tsep=' '): res += sep + splitted[1] 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

or

  • 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): '''French-accents-aware variant of string.lower.''' diff --git a/shared/xml_parser.py b/shared/xml_parser.py index 656d45d..12e6941 100644 --- a/shared/xml_parser.py +++ b/shared/xml_parser.py @@ -18,7 +18,7 @@ # 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,\ property_interning_dict from xml.sax.xmlreader import InputSource @@ -887,4 +887,127 @@ class XmlComparator: else: lastLinePrinted = False 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

    or

  • 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('%s' % 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' % (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) # ------------------------------------------------------------------------------