eggification
[PlinnDocument.git] / Products / PlinnDocument / PlinnDocument.py
diff --git a/Products/PlinnDocument/PlinnDocument.py b/Products/PlinnDocument/PlinnDocument.py
new file mode 100644 (file)
index 0000000..3267069
--- /dev/null
@@ -0,0 +1,273 @@
+# -*- coding: utf-8 -*-
+#######################################################################################
+#   Plinn - http://plinn.org                                                          #
+#   Copyright (C) 2005-2007  BenoĆ®t PIN <benoit.pin@ensmp.fr>                         #
+#                                                                                     #
+#   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('<img(.*?)>', 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('<img(.*?)src(.*?)=(.*?)"(?P<src>(.*?))"(.*?)>|<a(.*?)href(.*?)=(.*?)"(?P<href>(.*?))"(.*?)>', re.IGNORECASE)
+EMPTY_PLINN_DOCUMENT = '<plinn><rectangle width="800" height="600" elementKey="DIV_ELEMENT" ddOptions="2" ratio="undefined" visibility="visible"><upperLeftCorner><point x="0" y="0"/></upperLeftCorner><rawData/></rectangle></plinn>'
+
+
+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('<div class="plinn_document"/>')
+                               htmlDomDoc = htmlDom.documentElement
+                               
+                               self._transformRectangles(plinnElement, htmlDomDoc)
+                               firstChildStyle = htmlDomDoc.firstChild.getAttribute('style')
+                               htmlDomDoc.setAttribute('style', firstChildStyle.replace('absolute', 'relative'))
+                               
+                               if mergeLayers :
+                                       mergedDom = minidom.parseString('<div class="plinn_document"/>')
+                                       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('&lt;', '<').replace('&gt;', '>').replace('&quot;', '"').replace('&amp;', '&')
+                               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 '<img src="%(src)s/getThumbnail" width="%(width)s" height="%(height)s" alt="%(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