32670690db016c3fff3aea74a5cb28695d32596c
[PlinnDocument.git] / PlinnDocument.py
1 # -*- coding: utf-8 -*-
2 #######################################################################################
3 # Plinn - http://plinn.org #
4 # Copyright (C) 2005-2007 BenoƮt PIN <benoit.pin@ensmp.fr> #
5 # #
6 # This program is free software; you can redistribute it and/or #
7 # modify it under the terms of the GNU General Public License #
8 # as published by the Free Software Foundation; either version 2 #
9 # of the License, or (at your option) any later version. #
10 # #
11 # This program is distributed in the hope that it will be useful, #
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of #
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
14 # GNU General Public License for more details. #
15 # #
16 # You should have received a copy of the GNU General Public License #
17 # along with this program; if not, write to the Free Software #
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. #
19 #######################################################################################
20
21 from Globals import InitializeClass
22 from AccessControl import ClassSecurityInfo
23 from Products.CMFCore.permissions import View, ModifyPortalContent
24 from Products.CMFCore.utils import getToolByName
25 from Products.CMFDefault.Document import Document
26 from OFS.PropertyManager import PropertyManager
27 from OFS.Folder import Folder
28 from OFS.Image import File, cookId
29 from zope.component.factory import Factory
30 from zope.interface import implements
31 from Products.Photo import Photo
32 from Products.Plinn.utils import makeValidId
33 from interfaces import IPlinnDocument
34 from cStringIO import StringIO
35 from sets import Set
36 import xml.dom.minidom as minidom
37 import re
38
39 imgPattern = re.compile('<img(.*?)>', re.IGNORECASE)
40 imgWidthPattern = re.compile('style\s*=\s*".*width\s*:\s*([0-9]+)px')
41 imgHeightPattern = re.compile('style\s*=\s*".*height\s*:\s*([0-9]+)px')
42 imgSrcPattern = re.compile('src\s*=\s*"(.*)"')
43
44 imgOrLinkPattern = re.compile('<img(.*?)src(.*?)=(.*?)"(?P<src>(.*?))"(.*?)>|<a(.*?)href(.*?)=(.*?)"(?P<href>(.*?))"(.*?)>', re.IGNORECASE)
45 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>'
46
47
48 def addPlinnDocument(self, id, title='', description='', text=''):
49 """ Add a Plinn Document """
50 o = PlinnDocument(id, title, description, text)
51 self._setObject(id,o)
52
53 class PlinnDocument(Document) :
54 """ Plinn document - WYSIWYG editor
55 based on XML and javascript
56 """
57 implements(IPlinnDocument)
58
59 security = ClassSecurityInfo()
60
61 _cookedTexts = {}
62
63 def __init__(self, id, title='', description='', text='') :
64 self.attachments = Folder('attachments')
65 Document.__init__(self, id, title=title, description=description, text_format='html', text=text)
66
67 security.declareProtected(View, 'EditableBody')
68 def EditableBody(self, mergeLayers=True):
69 """ Transforms XML to HTML """
70
71 if self.text :
72 if not self._cookedTexts.has_key(self.absolute_url()) :
73 plinnElement = minidom.parseString(self.text).documentElement
74
75 htmlDom = minidom.parseString('<div class="plinn_document"/>')
76 htmlDomDoc = htmlDom.documentElement
77
78 self._transformRectangles(plinnElement, htmlDomDoc)
79 firstChildStyle = htmlDomDoc.firstChild.getAttribute('style')
80 htmlDomDoc.setAttribute('style', firstChildStyle.replace('absolute', 'relative'))
81
82 if mergeLayers :
83 mergedDom = minidom.parseString('<div class="plinn_document"/>')
84 mergedDomDoc = mergedDom.documentElement
85 for layer in htmlDomDoc.childNodes :
86 for foreignchild in layer.childNodes :
87 child = mergedDom.importNode(foreignchild, True)
88 mergedDomDoc.appendChild(child)
89
90 mergedDomDoc.setAttribute('style', htmlDomDoc.getAttribute('style'))
91 htmlDom = mergedDom
92
93 htmlText = htmlDom.toprettyxml().replace('&lt;', '<').replace('&gt;', '>').replace('&quot;', '"').replace('&amp;', '&')
94 htmlText = htmlText.encode('utf8')
95 htmlText = htmlText.split('\n', 1)[1]
96
97 htmlText = imgOrLinkPattern.sub(self._convertSrcOrHref, htmlText)
98 self._cookedTexts[self.absolute_url()] = htmlText
99 return htmlText
100 else :
101 return self._cookedTexts[self.absolute_url()]
102 else :
103 return ''
104
105 def _convertSrcOrHref(self, m) :
106 dict = m.groupdict()
107 if dict['src'] :
108 tag = m.group().replace(dict['src'], self._genAbsoluteUrl(dict['src']))
109 if not tag.endswith('/>') :
110 tag = tag[:-1] + '/>'
111 return tag
112 elif dict['href'] :
113 return m.group().replace(dict['href'], self._genAbsoluteUrl(dict['href']))
114 else:
115 return m.group()
116
117 def _genAbsoluteUrl(self, relUrl) :
118 if relUrl.find('attachments/') >=0 :
119 return self.absolute_url() + '/' + relUrl[relUrl.rindex('attachments/'):]
120 else :
121 return relUrl
122
123
124 security.declareProtected(ModifyPortalContent, 'XMLBody')
125 def XMLBody(self, REQUEST=None) :
126 """ return raw xml text """
127
128 if REQUEST is not None :
129 RESPONSE = REQUEST['RESPONSE']
130 RESPONSE.setHeader('content-type', 'text/xml; charset=utf-8')
131
132 manager = getToolByName(self, 'caching_policy_manager', None)
133 if manager is not None:
134 view_name = 'XMLBody'
135 headers = manager.getHTTPCachingHeaders(
136 self, view_name, {}
137 )
138
139 for key, value in headers:
140 if key == 'ETag':
141 RESPONSE.setHeader(key, value, literal=1)
142 else:
143 RESPONSE.setHeader(key, value)
144 if headers:
145 RESPONSE.setHeader('X-Cache-Headers-Set-By',
146 'CachingPolicyManager: %s' %
147 '/'.join(manager.getPhysicalPath()))
148
149
150 return Document.EditableBody(self) or EMPTY_PLINN_DOCUMENT
151
152
153 security.declareProtected(ModifyPortalContent, 'addAttachment')
154 def addAttachment(self, file, formId) :
155 """ Add attachment """
156 id, title = cookId('', '', file)
157
158 id = makeValidId(self.attachments, id)
159
160 if formId == 'ImageUploadForm':
161 fileOb = Photo(id, title, file, thumb_height=300, thumb_width=300)
162 else :
163 fileOb = File(id, title, '')
164 fileOb.manage_upload(file)
165
166 self.attachments._setObject(id, fileOb)
167 fileOb = getattr(self.attachments, id)
168 return fileOb
169
170
171 def _transformRectangles(self, inNode, outNode) :
172
173 for node in [ node for node in inNode.childNodes if node.nodeName == 'rectangle' ] :
174 if node.getAttribute('visibility') == 'hidden' :
175 continue
176
177 divRect = outNode.ownerDocument.createElement('div')
178 outNode.appendChild(divRect)
179
180 styleAttr = 'position:absolute'
181 styleAttr += ';width:%spx' % node.getAttribute('width')
182 styleAttr += ';height:%spx' % node.getAttribute('height')
183
184 for subNode in node.childNodes :
185 if subNode.nodeName == 'upperLeftCorner' :
186 for point in subNode.childNodes :
187 if point.nodeName == 'point' :
188 styleAttr += ';left:%spx' % point.getAttribute('x')
189 styleAttr += ';top:%spx' % point.getAttribute('y')
190 divRect.setAttribute('style', styleAttr)
191 break
192
193 elif subNode.nodeName == 'rawData' :
194 rawData = subNode.firstChild
195 if rawData :
196 textNode = outNode.ownerDocument.createTextNode(self.getElementTransform(node.getAttribute('elementKey'))(node, rawData.nodeValue))
197 divRect.appendChild(textNode)
198
199 self._transformRectangles(node, divRect)
200
201
202 security.declarePrivate('renderImg')
203 def renderImg(self, node, raw) :
204 width = int(node.getAttribute('width'))
205 height = int(node.getAttribute('height'))
206
207 photoId = raw.split('/')[-2]
208 photo = self._resizePhoto(photoId, width, height)
209
210 alt = 'image'
211 return '<img src="%(src)s/getThumbnail" width="%(width)s" height="%(height)s" alt="%(alt)s" />' % \
212 {'src' : photo.absolute_url(), 'width' : width, 'height' : height, 'alt' : alt}
213
214
215 security.declarePrivate('renderEpozImg')
216 def renderEpozImg(self, node, raw):
217 for img in imgPattern.findall(raw) :
218 width = imgWidthPattern.findall(img)
219 if width : width = int(width[0])
220
221 height = imgHeightPattern.findall(img)
222 if height : height = int(height[0])
223
224 if not (width or height) : continue # default size
225
226 photoId = imgSrcPattern.findall(img)[0].split('/')[-2]
227 self._resizePhoto(photoId, width, height)
228
229 return raw
230
231
232 def _resizePhoto(self, photoId, width, height):
233 photo = getattr(self.attachments, photoId)
234
235 nts = [width, height]
236 landscape = width > height
237 if landscape :
238 nts.reverse()
239
240 thumbSize = {'width': photo.thumb_width, 'height': photo.thumb_height}
241
242 if thumbSize['width'] != nts[0] or thumbSize['height'] != nts[1] > 1 :
243 photo.manage_editProperties(thumb_width=nts[0], thumb_height=nts[1])
244
245 return photo
246
247
248 security.declarePrivate('getElementTransform')
249 def getElementTransform(self, elementKey) :
250 transforms = {'IMG_ELEMENT': self.renderImg,
251 'EPOZ_ELEMENT': self.renderEpozImg}
252 return transforms.get(elementKey, lambda node, raw : raw)
253
254 def _edit(self, text):
255 """ Edit the Document and cook the body.
256 """
257 Document._edit(self, text)
258 self._removeUnusedAttachments()
259 self._cookedTexts = {}
260
261
262 def _removeUnusedAttachments(self) :
263 if not(self.attachments.objectIds() and self.XMLBody()) : return
264
265 reAttachments = re.compile('|'.join( [r'\b%s\b' % id for id in self.attachments.objectIds()] ))
266 xml = self.XMLBody()
267 attSet = Set(self.attachments.objectIds())
268 useSet = Set(reAttachments.findall(xml))
269 self.attachments.manage_delObjects([att for att in attSet - useSet])
270
271
272 InitializeClass(PlinnDocument)
273 PlinnDocumentFactory = Factory(PlinnDocument)