1 # -*- coding: utf-8 -*-
2 #######################################################################################
3 # Plinn - http://plinn.org #
4 # Copyright (C) 2005-2007 BenoƮt PIN <benoit.pin@ensmp.fr> #
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. #
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. #
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 #######################################################################################
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
36 import xml
.dom
.minidom
as minidom
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*"(.*)"')
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>'
48 def addPlinnDocument(self
, id, title
='', description
='', text
=''):
49 """ Add a Plinn Document """
50 o
= PlinnDocument(id, title
, description
, text
)
53 class PlinnDocument(Document
) :
54 """ Plinn document - WYSIWYG editor
55 based on XML and javascript
57 implements(IPlinnDocument
)
59 security
= ClassSecurityInfo()
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
)
67 security
.declareProtected(View
, 'EditableBody')
68 def EditableBody(self
, mergeLayers
=True):
69 """ Transforms XML to HTML """
72 if not self
._cookedTexts
.has_key(self
.absolute_url()) :
73 plinnElement
= minidom
.parseString(self
.text
).documentElement
75 htmlDom
= minidom
.parseString('<div class="plinn_document"/>')
76 htmlDomDoc
= htmlDom
.documentElement
78 self
._transformRectangles
(plinnElement
, htmlDomDoc
)
79 firstChildStyle
= htmlDomDoc
.firstChild
.getAttribute('style')
80 htmlDomDoc
.setAttribute('style', firstChildStyle
.replace('absolute', 'relative'))
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
)
90 mergedDomDoc
.setAttribute('style', htmlDomDoc
.getAttribute('style'))
93 htmlText
= htmlDom
.toprettyxml().replace('<', '<').replace('>', '>').replace('"', '"').replace('&', '&')
94 htmlText
= htmlText
.encode('utf8')
95 htmlText
= htmlText
.split('\n', 1)[1]
97 htmlText
= imgOrLinkPattern
.sub(self
._convertSrcOrHref
, htmlText
)
98 self
._cookedTexts
[self
.absolute_url()] = htmlText
101 return self
._cookedTexts
[self
.absolute_url()]
105 def _convertSrcOrHref(self
, m
) :
108 tag
= m
.group().replace(dict['src'], self
._genAbsoluteUrl
(dict['src']))
109 if not tag
.endswith('/>') :
110 tag
= tag
[:-1] + '/>'
113 return m
.group().replace(dict['href'], self
._genAbsoluteUrl
(dict['href']))
117 def _genAbsoluteUrl(self
, relUrl
) :
118 if relUrl
.find('attachments/') >=0 :
119 return self
.absolute_url() + '/' + relUrl
[relUrl
.rindex('attachments/'):]
124 security
.declareProtected(ModifyPortalContent
, 'XMLBody')
125 def XMLBody(self
, REQUEST
=None) :
126 """ return raw xml text """
128 if REQUEST
is not None :
129 RESPONSE
= REQUEST
['RESPONSE']
130 RESPONSE
.setHeader('content-type', 'text/xml; charset=utf-8')
132 manager
= getToolByName(self
, 'caching_policy_manager', None)
133 if manager
is not None:
134 view_name
= 'XMLBody'
135 headers
= manager
.getHTTPCachingHeaders(
139 for key
, value
in headers
:
141 RESPONSE
.setHeader(key
, value
, literal
=1)
143 RESPONSE
.setHeader(key
, value
)
145 RESPONSE
.setHeader('X-Cache-Headers-Set-By',
146 'CachingPolicyManager: %s' %
147 '/'.join(manager
.getPhysicalPath()))
150 return Document
.EditableBody(self
) or EMPTY_PLINN_DOCUMENT
153 security
.declareProtected(ModifyPortalContent
, 'addAttachment')
154 def addAttachment(self
, file, formId
) :
155 """ Add attachment """
156 id, title
= cookId('', '', file)
158 id = makeValidId(self
.attachments
, id)
160 if formId
== 'ImageUploadForm':
161 fileOb
= Photo(id, title
, file, thumb_height
=300, thumb_width
=300)
163 fileOb
= File(id, title
, '')
164 fileOb
.manage_upload(file)
166 self
.attachments
._setObject
(id, fileOb
)
167 fileOb
= getattr(self
.attachments
, id)
171 def _transformRectangles(self
, inNode
, outNode
) :
173 for node
in [ node
for node
in inNode
.childNodes
if node
.nodeName
== 'rectangle' ] :
174 if node
.getAttribute('visibility') == 'hidden' :
177 divRect
= outNode
.ownerDocument
.createElement('div')
178 outNode
.appendChild(divRect
)
180 styleAttr
= 'position:absolute'
181 styleAttr
+= ';width:%spx' % node
.getAttribute('width')
182 styleAttr
+= ';height:%spx' % node
.getAttribute('height')
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
)
193 elif subNode
.nodeName
== 'rawData' :
194 rawData
= subNode
.firstChild
196 textNode
= outNode
.ownerDocument
.createTextNode(self
.getElementTransform(node
.getAttribute('elementKey'))(node
, rawData
.nodeValue
))
197 divRect
.appendChild(textNode
)
199 self
._transformRectangles
(node
, divRect
)
202 security
.declarePrivate('renderImg')
203 def renderImg(self
, node
, raw
) :
204 width
= int(node
.getAttribute('width'))
205 height
= int(node
.getAttribute('height'))
207 photoId
= raw
.split('/')[-2]
208 photo
= self
._resizePhoto
(photoId
, width
, height
)
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
}
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])
221 height
= imgHeightPattern
.findall(img
)
222 if height
: height
= int(height
[0])
224 if not (width
or height
) : continue # default size
226 photoId
= imgSrcPattern
.findall(img
)[0].split('/')[-2]
227 self
._resizePhoto
(photoId
, width
, height
)
232 def _resizePhoto(self
, photoId
, width
, height
):
233 photo
= getattr(self
.attachments
, photoId
)
235 nts
= [width
, height
]
236 landscape
= width
> height
240 thumbSize
= {'width': photo
.thumb_width
, 'height': photo
.thumb_height
}
242 if thumbSize
['width'] != nts
[0] or thumbSize
['height'] != nts
[1] > 1 :
243 photo
.manage_editProperties(thumb_width
=nts
[0], thumb_height
=nts
[1])
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
)
254 def _edit(self
, text
):
255 """ Edit the Document and cook the body.
257 Document
._edit
(self
, text
)
258 self
._removeUnusedAttachments
()
259 self
._cookedTexts
= {}
262 def _removeUnusedAttachments(self
) :
263 if not(self
.attachments
.objectIds() and self
.XMLBody()) : return
265 reAttachments
= re
.compile('|'.join( [r
'\b%s\b' % id for id in self
.attachments
.objectIds()] ))
267 attSet
= Set(self
.attachments
.objectIds())
268 useSet
= Set(reAttachments
.findall(xml
))
269 self
.attachments
.manage_delObjects([att
for att
in attSet
- useSet
])
272 InitializeClass(PlinnDocument
)
273 PlinnDocumentFactory
= Factory(PlinnDocument
)