X-Git-Url: https://scm.cri.ensmp.fr/git/PlinnDocument.git/blobdiff_plain/57a4d385a1d2806d5877f53b1fdb0bd94efa2dbb..3b91dbcbb0b99d3d796a01813018db0e540bd0ec:/Products/PlinnDocument/PlinnDocument.py?ds=sidebyside diff --git a/Products/PlinnDocument/PlinnDocument.py b/Products/PlinnDocument/PlinnDocument.py new file mode 100644 index 0000000..3267069 --- /dev/null +++ b/Products/PlinnDocument/PlinnDocument.py @@ -0,0 +1,273 @@ +# -*- coding: utf-8 -*- +####################################################################################### +# Plinn - http://plinn.org # +# Copyright (C) 2005-2007 BenoƮt PIN # +# # +# 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 Globals import InitializeClass +from AccessControl import ClassSecurityInfo +from Products.CMFCore.permissions import View, ModifyPortalContent +from Products.CMFCore.utils import getToolByName +from Products.CMFDefault.Document import Document +from OFS.PropertyManager import PropertyManager +from OFS.Folder import Folder +from OFS.Image import File, cookId +from zope.component.factory import Factory +from zope.interface import implements +from Products.Photo import Photo +from Products.Plinn.utils import makeValidId +from interfaces import IPlinnDocument +from cStringIO import StringIO +from sets import Set +import xml.dom.minidom as minidom +import re + +imgPattern = re.compile('', re.IGNORECASE) +imgWidthPattern = re.compile('style\s*=\s*".*width\s*:\s*([0-9]+)px') +imgHeightPattern = re.compile('style\s*=\s*".*height\s*:\s*([0-9]+)px') +imgSrcPattern = re.compile('src\s*=\s*"(.*)"') + +imgOrLinkPattern = re.compile('(.*?))"(.*?)>|(.*?))"(.*?)>', re.IGNORECASE) +EMPTY_PLINN_DOCUMENT = '' + + +def addPlinnDocument(self, id, title='', description='', text=''): + """ Add a Plinn Document """ + o = PlinnDocument(id, title, description, text) + self._setObject(id,o) + +class PlinnDocument(Document) : + """ Plinn document - WYSIWYG editor + based on XML and javascript + """ + implements(IPlinnDocument) + + security = ClassSecurityInfo() + + _cookedTexts = {} + + def __init__(self, id, title='', description='', text='') : + self.attachments = Folder('attachments') + Document.__init__(self, id, title=title, description=description, text_format='html', text=text) + + security.declareProtected(View, 'EditableBody') + def EditableBody(self, mergeLayers=True): + """ Transforms XML to HTML """ + + if self.text : + if not self._cookedTexts.has_key(self.absolute_url()) : + plinnElement = minidom.parseString(self.text).documentElement + + htmlDom = minidom.parseString('
') + htmlDomDoc = htmlDom.documentElement + + self._transformRectangles(plinnElement, htmlDomDoc) + firstChildStyle = htmlDomDoc.firstChild.getAttribute('style') + htmlDomDoc.setAttribute('style', firstChildStyle.replace('absolute', 'relative')) + + if mergeLayers : + mergedDom = minidom.parseString('
') + mergedDomDoc = mergedDom.documentElement + for layer in htmlDomDoc.childNodes : + for foreignchild in layer.childNodes : + child = mergedDom.importNode(foreignchild, True) + mergedDomDoc.appendChild(child) + + mergedDomDoc.setAttribute('style', htmlDomDoc.getAttribute('style')) + htmlDom = mergedDom + + htmlText = htmlDom.toprettyxml().replace('<', '<').replace('>', '>').replace('"', '"').replace('&', '&') + htmlText = htmlText.encode('utf8') + htmlText = htmlText.split('\n', 1)[1] + + htmlText = imgOrLinkPattern.sub(self._convertSrcOrHref, htmlText) + self._cookedTexts[self.absolute_url()] = htmlText + return htmlText + else : + return self._cookedTexts[self.absolute_url()] + else : + return '' + + def _convertSrcOrHref(self, m) : + dict = m.groupdict() + if dict['src'] : + tag = m.group().replace(dict['src'], self._genAbsoluteUrl(dict['src'])) + if not tag.endswith('/>') : + tag = tag[:-1] + '/>' + return tag + elif dict['href'] : + return m.group().replace(dict['href'], self._genAbsoluteUrl(dict['href'])) + else: + return m.group() + + def _genAbsoluteUrl(self, relUrl) : + if relUrl.find('attachments/') >=0 : + return self.absolute_url() + '/' + relUrl[relUrl.rindex('attachments/'):] + else : + return relUrl + + + security.declareProtected(ModifyPortalContent, 'XMLBody') + def XMLBody(self, REQUEST=None) : + """ return raw xml text """ + + if REQUEST is not None : + RESPONSE = REQUEST['RESPONSE'] + RESPONSE.setHeader('content-type', 'text/xml; charset=utf-8') + + manager = getToolByName(self, 'caching_policy_manager', None) + if manager is not None: + view_name = 'XMLBody' + headers = manager.getHTTPCachingHeaders( + self, view_name, {} + ) + + for key, value in headers: + if key == 'ETag': + RESPONSE.setHeader(key, value, literal=1) + else: + RESPONSE.setHeader(key, value) + if headers: + RESPONSE.setHeader('X-Cache-Headers-Set-By', + 'CachingPolicyManager: %s' % + '/'.join(manager.getPhysicalPath())) + + + return Document.EditableBody(self) or EMPTY_PLINN_DOCUMENT + + + security.declareProtected(ModifyPortalContent, 'addAttachment') + def addAttachment(self, file, formId) : + """ Add attachment """ + id, title = cookId('', '', file) + + id = makeValidId(self.attachments, id) + + if formId == 'ImageUploadForm': + fileOb = Photo(id, title, file, thumb_height=300, thumb_width=300) + else : + fileOb = File(id, title, '') + fileOb.manage_upload(file) + + self.attachments._setObject(id, fileOb) + fileOb = getattr(self.attachments, id) + return fileOb + + + def _transformRectangles(self, inNode, outNode) : + + for node in [ node for node in inNode.childNodes if node.nodeName == 'rectangle' ] : + if node.getAttribute('visibility') == 'hidden' : + continue + + divRect = outNode.ownerDocument.createElement('div') + outNode.appendChild(divRect) + + styleAttr = 'position:absolute' + styleAttr += ';width:%spx' % node.getAttribute('width') + styleAttr += ';height:%spx' % node.getAttribute('height') + + for subNode in node.childNodes : + if subNode.nodeName == 'upperLeftCorner' : + for point in subNode.childNodes : + if point.nodeName == 'point' : + styleAttr += ';left:%spx' % point.getAttribute('x') + styleAttr += ';top:%spx' % point.getAttribute('y') + divRect.setAttribute('style', styleAttr) + break + + elif subNode.nodeName == 'rawData' : + rawData = subNode.firstChild + if rawData : + textNode = outNode.ownerDocument.createTextNode(self.getElementTransform(node.getAttribute('elementKey'))(node, rawData.nodeValue)) + divRect.appendChild(textNode) + + self._transformRectangles(node, divRect) + + + security.declarePrivate('renderImg') + def renderImg(self, node, raw) : + width = int(node.getAttribute('width')) + height = int(node.getAttribute('height')) + + photoId = raw.split('/')[-2] + photo = self._resizePhoto(photoId, width, height) + + alt = 'image' + return '%(alt)s' % \ + {'src' : photo.absolute_url(), 'width' : width, 'height' : height, 'alt' : alt} + + + security.declarePrivate('renderEpozImg') + def renderEpozImg(self, node, raw): + for img in imgPattern.findall(raw) : + width = imgWidthPattern.findall(img) + if width : width = int(width[0]) + + height = imgHeightPattern.findall(img) + if height : height = int(height[0]) + + if not (width or height) : continue # default size + + photoId = imgSrcPattern.findall(img)[0].split('/')[-2] + self._resizePhoto(photoId, width, height) + + return raw + + + def _resizePhoto(self, photoId, width, height): + photo = getattr(self.attachments, photoId) + + nts = [width, height] + landscape = width > height + if landscape : + nts.reverse() + + thumbSize = {'width': photo.thumb_width, 'height': photo.thumb_height} + + if thumbSize['width'] != nts[0] or thumbSize['height'] != nts[1] > 1 : + photo.manage_editProperties(thumb_width=nts[0], thumb_height=nts[1]) + + return photo + + + security.declarePrivate('getElementTransform') + def getElementTransform(self, elementKey) : + transforms = {'IMG_ELEMENT': self.renderImg, + 'EPOZ_ELEMENT': self.renderEpozImg} + return transforms.get(elementKey, lambda node, raw : raw) + + def _edit(self, text): + """ Edit the Document and cook the body. + """ + Document._edit(self, text) + self._removeUnusedAttachments() + self._cookedTexts = {} + + + def _removeUnusedAttachments(self) : + if not(self.attachments.objectIds() and self.XMLBody()) : return + + reAttachments = re.compile('|'.join( [r'\b%s\b' % id for id in self.attachments.objectIds()] )) + xml = self.XMLBody() + attSet = Set(self.attachments.objectIds()) + useSet = Set(reAttachments.findall(xml)) + self.attachments.manage_delObjects([att for att in attSet - useSet]) + + +InitializeClass(PlinnDocument) +PlinnDocumentFactory = Factory(PlinnDocument) \ No newline at end of file