import initial.
[Epoz.git] / __init__.py
1 #####
2 ### Epoz - a cross-browser-wysiwyg-editor for Zope
3 ## Copyright (C) 2005 Maik Jablonski (maik.jablonski@uni-bielefeld.de)
4 #
5 # All Rights Reserved.
6 #
7 # This software is subject to the provisions of the Zope Public License,
8 # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
9 # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
10 # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
11 ## WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
12 ### FOR A PARTICULAR PURPOSE.
13 ####
14
15 import re
16 import urlparse
17 import cgi
18
19 # try to import mxTidy as default
20 try:
21 from mx import Tidy
22 mxTidyIsAvailable = 1
23 except:
24 mxTidyIsAvailable = 0
25
26 # try to import uTidylib as fallback
27 try:
28 import tidy
29 uTidyIsAvailable = 1
30 except ImportError:
31 uTidyIsAvailable = 0
32
33 import Globals
34 from Products.PageTemplates.PageTemplateFile import PageTemplateFile
35 from App.ImageFile import ImageFile
36
37 # Regexp for snipping the body of a document
38 HTML_BODY = re.compile('<body[^>]*?>(.*)</body[^>]*?>', re.DOTALL|re.IGNORECASE)
39 MSO_CLASS = re.compile(r'<(\w+)\W+(class="Mso.*?")>', re.IGNORECASE)
40 EPOZ_SCRIPT = re.compile(r'<epoz:script (style="[^"]*")? ?')
41
42 misc_ = {
43 # The Format-Controls
44 'epoz_script_main.js':ImageFile('epoz/epoz_core/epoz_script_main.js.dtml', globals()),
45
46 # Rendering of the widget
47 'epoz_script_widget.js':ImageFile('epoz/epoz_core/epoz_script_widget.js.dtml', globals()),
48
49 # Browser-detection
50 'epoz_script_detect.js':ImageFile('epoz/epoz_core/epoz_script_detect.js.dtml', globals()),
51
52 # Code for the color-selection-popup
53 'epoz_script_color.html':ImageFile('epoz/epoz_core/epoz_script_color.html.dtml', globals()),
54
55 # Code for the table-popup
56 'epoz_script_table.html':ImageFile('epoz/epoz_core/epoz_script_table.html.dtml', globals()),
57
58 # The XMLRPC-handler
59 'vcXMLRPC.js':ImageFile('epoz/epoz_core/vcXMLRPC.js.dtml', globals()),
60
61 # Multi-Epoz
62 'epoz_redirect.js':ImageFile('epoz/epoz_core/epoz_redirect.js.dtml', globals()),
63 'epoz_iframe_trigger.js':ImageFile('epoz/epoz_core/epoz_iframe_trigger.js.dtml', globals()),
64
65 # The Epoz-Language-Files
66 'epoz_lang_da.js':ImageFile('epoz/epoz_i18n/epoz_lang_da.js.dtml', globals()),
67 'epoz_lang_de.js':ImageFile('epoz/epoz_i18n/epoz_lang_de.js.dtml', globals()),
68 'epoz_lang_en.js':ImageFile('epoz/epoz_i18n/epoz_lang_en.js.dtml', globals()),
69 'epoz_lang_es.js':ImageFile('epoz/epoz_i18n/epoz_lang_es.js.dtml', globals()),
70 'epoz_lang_fi.js':ImageFile('epoz/epoz_i18n/epoz_lang_fi.js.dtml', globals()),
71 'epoz_lang_fr.js':ImageFile('epoz/epoz_i18n/epoz_lang_fr.js.dtml', globals()),
72 'epoz_lang_hu.js':ImageFile('epoz/epoz_i18n/epoz_lang_hu.js.dtml', globals()),
73 'epoz_lang_it.js':ImageFile('epoz/epoz_i18n/epoz_lang_it.js.dtml', globals()),
74 'epoz_lang_nl.js':ImageFile('epoz/epoz_i18n/epoz_lang_nl.js.dtml', globals()),
75 'epoz_lang_no.js':ImageFile('epoz/epoz_i18n/epoz_lang_no.js.dtml', globals()),
76 'epoz_lang_pl.js':ImageFile('epoz/epoz_i18n/epoz_lang_pl.js.dtml', globals()),
77 'epoz_lang_pt.js':ImageFile('epoz/epoz_i18n/epoz_lang_pt.js.dtml', globals()),
78 'epoz_lang_pt-br.js':ImageFile('epoz/epoz_i18n/epoz_lang_pt-br.js.dtml', globals()),
79 'epoz_lang_ru.js':ImageFile('epoz/epoz_i18n/epoz_lang_ru.js.dtml', globals()),
80 'epoz_lang_zh-cn.js':ImageFile('epoz/epoz_i18n/epoz_lang_zh-cn.js.dtml', globals()),
81
82 # Generic language file transalted by PlacelessTranslationService (for example)
83 'epoz_multilingual.js':PageTemplateFile('epoz/epoz_core/epoz_multilingual.js.pt', globals()),
84
85 # The Epoz-Buttons
86 'epoz_button_anchor.gif':ImageFile('epoz/epoz_images/epoz_button_anchor.gif', globals()),
87 'epoz_button_bgcolor.gif':ImageFile('epoz/epoz_images/epoz_button_bgcolor.gif', globals()),
88 'epoz_button_bold.gif':ImageFile('epoz/epoz_images/epoz_button_bold.gif', globals()),
89 'epoz_button_centre.gif':ImageFile('epoz/epoz_images/epoz_button_centre.gif', globals()),
90 'epoz_button_hr.gif':ImageFile('epoz/epoz_images/epoz_button_hr.gif', globals()),
91 'epoz_button_hyperlink.gif':ImageFile('epoz/epoz_images/epoz_button_hyperlink.gif', globals()),
92 'epoz_button_image.gif':ImageFile('epoz/epoz_images/epoz_button_image.gif', globals()),
93 'epoz_button_indent.gif':ImageFile('epoz/epoz_images/epoz_button_indent.gif', globals()),
94 'epoz_button_italic.gif':ImageFile('epoz/epoz_images/epoz_button_italic.gif', globals()),
95 'epoz_button_left_just.gif':ImageFile('epoz/epoz_images/epoz_button_left_just.gif', globals()),
96 'epoz_button_list.gif':ImageFile('epoz/epoz_images/epoz_button_list.gif', globals()),
97 'epoz_button_numbered_list.gif':ImageFile('epoz/epoz_images/epoz_button_numbered_list.gif', globals()),
98 'epoz_button_outdent.gif':ImageFile('epoz/epoz_images/epoz_button_outdent.gif', globals()),
99 'epoz_button_redo.gif':ImageFile('epoz/epoz_images/epoz_button_redo.gif', globals()),
100 'epoz_button_right_just.gif':ImageFile('epoz/epoz_images/epoz_button_right_just.gif', globals()),
101 'epoz_button_strikethrough.gif':ImageFile('epoz/epoz_images/epoz_button_strikethrough.gif', globals()),
102 'epoz_button_subscript.gif':ImageFile('epoz/epoz_images/epoz_button_subscript.gif', globals()),
103 'epoz_button_superscript.gif':ImageFile('epoz/epoz_images/epoz_button_superscript.gif', globals()),
104 'epoz_button_table.gif':ImageFile('epoz/epoz_images/epoz_button_table.gif', globals()),
105 'epoz_button_textcolor.gif':ImageFile('epoz/epoz_images/epoz_button_textcolor.gif', globals()),
106 'epoz_button_tools.gif':ImageFile('epoz/epoz_images/epoz_button_tools.gif', globals()),
107 'epoz_button_underline.gif':ImageFile('epoz/epoz_images/epoz_button_underline.gif', globals()),
108 'epoz_button_undo.gif':ImageFile('epoz/epoz_images/epoz_button_undo.gif', globals()),
109 'epoz_button_unformat.gif':ImageFile('epoz/epoz_images/epoz_button_unformat.gif', globals()),
110 }
111
112
113 def Epoz(self, name, data='', toolbox='', lang='auto',
114 path='', widget='',
115 style='width: 600px; height: 250px; border: 1px solid #000000;',
116 button='background-color: #EFEFEF; border: 1px solid #A0A0A0; cursor: pointer; margin-right: 1px; margin-bottom: 1px;',
117 css='', customcss='', charset='utf-8', pageurl=''):
118 """ Create an Epoz-Wysiwyg-Editor.
119
120 name : the name of the form-element which submits the data
121 data : the data to edit
122 toolbox : a link to a HTML-Page which delivers additional tools
123 lang: a code for the language-file (en,de,...).
124 Use auto to enable browser language detection.
125 In this case, a localization product must be installed (like PlacelessTranslationService).
126 path : path to Epoz-Javascript. Needed mainly for Plone (portal_url).
127 widget: You can specify a path to an alternative JavaScript for
128 epoz_script_widget.js
129 style : style-definition for the editor-area
130 button : style-definiton for buttons
131 css : url to a global css which should be used for rendering
132 content in editor-area
133 customcss: url to a customized css which should be used for rendering
134 content in editor-area
135 charset : charset which is edited in the editor-area
136 pageurl: the base-url for the edited page
137
138 If Epoz can't create a Rich-Text-Editor, a simple textarea is created.
139 """
140
141 if data is None:
142 data=''
143
144 js_data = data
145 data = cgi.escape(data)
146
147 # hide scripts without removing them
148 js_data = js_data.replace('<script ',
149 '<epoz:script style="display: none" ')
150 js_data = js_data.replace('</script>', '</epoz:script>')
151
152 # Quote newlines and single quotes, so the Epoz-JavaScript won't break.
153 # Needs to be a list and no dictionary, cause we need order!!!
154
155 quotes = (("\\","\\\\"), ("\n","\\n"), ("\r","\\r"), ("'","\\'"))
156
157 for item in quotes:
158 js_data = js_data.replace(item[0], item[1])
159
160 # Determine the correct path for VirtualHostMonsters & PCGI & etc.pp.
161 if not path:
162 path = "%s/misc_/Epoz/" % (self.REQUEST.get('BASEPATH1',''),)
163
164 if not pageurl:
165 pageurl = self.REQUEST.get('URL1','')
166
167 if lang == 'auto' :
168 i18n='<script language="JavaScript" type="text/javascript" src="%(path)sepoz_multilingual.js?charset=%(charset)s" charset="%(charset)s"></script>' % {'path' : path, 'charset' : charset}
169 else :
170 i18n='<script language="JavaScript" type="text/javascript" src="%sepoz_lang_en.js"></script>' % (path,)
171 if lang<>'en':
172 i18n += '<script language="JavaScript" type="text/javascript" src="%sepoz_lang_%s.js"></script>' % (path, lang)
173
174 if not widget:
175 widget = "%sepoz_script_widget.js" % path
176
177 # This is just a dummy page for test-iframe
178 # to prevent IE complaining when using Epoz via https
179 iframesrc = pageurl+'/epoz_blank_iframe.html'
180
181 # Return the HTML-Code for the Epoz-Rich-Text-Editor
182 return """
183 %(i18n)s
184 <script language="JavaScript" type="text/javascript" src="%(widget)s"></script>
185 <script language="JavaScript" type="text/javascript" src="%(path)svcXMLRPC.js"></script>
186 <script language="JavaScript" type="text/javascript" src="%(path)sepoz_script_detect.js"></script>
187 <script language="JavaScript" type="text/javascript" src="%(path)sepoz_script_main.js" charset="utf-8"></script>
188 <script language="JavaScript" type="text/javascript" src="%(path)sepoz_redirect.js"></script>
189 <script language="JavaScript" type="text/javascript">
190 <!--
191 InitIframe('%(name)s','%(js_data)s','%(path)s','%(toolbox)s','%(style)s','%(button)s','%(css)s','%(customcss)s','%(charset)s', '%(pageurl)s');
192 //-->
193 </script>
194 <noscript><textarea name="%(name)s" style="%(style)s">%(data)s</textarea></noscript>
195 """ % {
196 'i18n': i18n,
197 'widget': widget,
198 'path': path,
199 'name': name,
200 'js_data': js_data,
201 'toolbox': toolbox,
202 'style': style,
203 'button': button,
204 'css': css,
205 'customcss': customcss,
206 'charset': charset,
207 'data': data,
208 'pageurl': pageurl,
209 'iframesrc': iframesrc
210 }
211
212
213
214 def EpozTidy(self, html, pageurl):
215 """ Take html and deliver xhtml if mxTidy is installed;
216 call EpozPostTidy for html-postprocessings before returning the result
217 """
218
219 errors = 0
220 output = html
221 errordata = ""
222
223 input = html.encode("utf-8")
224 input = EPOZ_SCRIPT.sub('<script ', input)
225 input = input.replace('</epoz:script>', '</script>')
226
227 if mxTidyIsAvailable:
228 (errors, warnings, output, errordata) = Tidy.tidy(
229 input, drop_empty_paras=1, indent_spaces=1, indent="auto",
230 output_xhtml=1, word_2000=1, wrap=79, char_encoding="utf8")
231 if errors:
232 output = html
233 elif uTidyIsAvailable:
234 parsed = tidy.parseString(
235 input, drop_empty_paras=1, indent_spaces=1, indent="auto",
236 output_xhtml=1, word_2000=1, wrap=79, char_encoding="utf8",
237 add_xml_decl=0, doctype="omit", indent_attributes=1,
238 drop_proprietary_attributes=1, bare=1, clean=1,
239 enclose_text=1, tidy_mark=0)
240 reports = parsed.get_errors()
241 all_errors = [str(x) for x in reports if x.severity != 'W']
242 errors = len(all_errors)
243 errordata = '\n'.join(all_errors)
244 if errors:
245 output = html
246 else:
247 output = str(parsed)
248
249 output = MSO_CLASS.sub(r"<\1>", output)
250 result = HTML_BODY.search(output)
251 if result:
252 output = result.group(1)
253
254 # Call External Method / PythonScript for postprocessing
255 # The script should expect two parameters:
256 # self = called context (=server)
257 # html = the htmlbody to postprocess
258 # pathname = path of edited object (maybe with template!)
259 # The script should return the new htmlbody
260
261 EpozPostTidy = getattr(self, 'EpozPostTidy', None)
262 if EpozPostTidy is not None:
263 output = EpozPostTidy(self, output, pageurl)
264
265 return (errors, output, errordata)
266
267
268 def EpozGetRelativeUrl(self, pageUrl, targetUrl):
269 """ Returns relative path from targetUrl to pageUrl. """
270
271 # Just some shortcuts...
272 SCHEME, HOSTNAME, PATH, PARAMS, QUERY, FRAGMENT = range(6)
273
274 pageUnits = urlparse.urlparse(pageUrl)
275 targetUnits = urlparse.urlparse(targetUrl)
276
277 if pageUnits[:PATH] != targetUnits[:PATH]:
278 # Scheme (http) or host:port (www.comain.com) are different
279 # -> no possible relative URL
280 return targetUrl
281
282 afterPath = targetUnits[PARAMS:]
283
284 pagePath = pageUnits[PATH].split('/')
285 pagePath.reverse()
286
287 targetPath = targetUnits[PATH].split('/')
288 targetPath.reverse()
289
290 # Remove parts which are equal (['a', 'b'] ['a', 'c'] --> ['c'])
291 while (len(pagePath) > 0
292 and len(targetPath) > 0
293 and pagePath[-1] == targetPath[-1]):
294 pagePath.pop()
295 last_pop = targetPath.pop()
296 if len(pagePath) == 0 and len(targetPath) == 0:
297 # Special case: link to itself (http://foo/a http://foo/a -> a)
298 targetPath.append(last_pop)
299 if len(pagePath) == 1 and pagePath[0] == "" and len(targetPath) == 0:
300 # Special case: (http://foo/a/ http://foo/a -> ../a)
301 pagePath.append(last_pop)
302 targetPath.append(last_pop)
303
304 targetPath.reverse()
305
306 relativeUrl = ['..'] * (len(pagePath) -1)
307 relativeUrl = ('', '', '/'.join(relativeUrl + targetPath)) + tuple(afterPath)
308
309 return urlparse.urlunparse(relativeUrl)
310
311
312 # Make sure the Epoz method is replaceable in the Products folder
313 Epoz.__replaceable__ = Globals.REPLACEABLE
314
315 # Register Epoz as global method
316 # Register EpozTidy as global method for XMLRPC
317 # Register EpozGetRelativeUrl as global method
318 # Register epoz_blank_iframe.html as global PageTemplate
319
320 methods = {
321 'Epoz': Epoz,
322 'Epoz__roles__': None,
323
324 'EpozTidy': EpozTidy,
325 'EpozTidy__roles__': None,
326
327 'EpozGetRelativeUrl': EpozGetRelativeUrl,
328 'EpozGetRelativeUrl__roles__': None,
329
330 'epoz_blank_iframe.html': PageTemplateFile('epoz/epoz_core/epoz_blank_iframe.html.pt', globals()),
331 'epoz_blank_iframe.html__roles__': None,
332 }
333
334
335 # And now try to register Epoz for CMF/Plone
336
337 try:
338 from Products.CMFCore.DirectoryView import registerDirectory
339
340 global cmfepoz_globals
341 cmfepoz_globals=globals()
342
343 def initialize(context):
344 registerDirectory('epoz', cmfepoz_globals)
345 except:
346 pass
347