2 ### Epoz - a cross-browser-wysiwyg-editor for Zope
3 ## Copyright (C) 2005 Maik Jablonski (maik.jablonski@uni-bielefeld.de)
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.
19 # try to import mxTidy as default
26 # try to import uTidylib as fallback
34 from Products
.PageTemplates
.PageTemplateFile
import PageTemplateFile
35 from App
.ImageFile
import ImageFile
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="[^"]*")? ?')
44 'epoz_script_main.js':ImageFile('epoz/epoz_core/epoz_script_main.js.dtml', globals()),
46 # Rendering of the widget
47 'epoz_script_widget.js':ImageFile('epoz/epoz_core/epoz_script_widget.js.dtml', globals()),
50 'epoz_script_detect.js':ImageFile('epoz/epoz_core/epoz_script_detect.js.dtml', globals()),
52 # Code for the color-selection-popup
53 'epoz_script_color.html':ImageFile('epoz/epoz_core/epoz_script_color.html.dtml', globals()),
55 # Code for the table-popup
56 'epoz_script_table.html':ImageFile('epoz/epoz_core/epoz_script_table.html.dtml', globals()),
59 'vcXMLRPC.js':ImageFile('epoz/epoz_core/vcXMLRPC.js.dtml', globals()),
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()),
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()),
82 # Generic language file transalted by PlacelessTranslationService (for example)
83 'epoz_multilingual.js':PageTemplateFile('epoz/epoz_core/epoz_multilingual.js.pt', globals()),
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()),
113 def Epoz(self
, name
, data
='', toolbox
='', lang
='auto',
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.
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
138 If Epoz can't create a Rich-Text-Editor, a simple textarea is created.
145 data
= cgi
.escape(data
)
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>')
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!!!
155 quotes
= (("\\","\\\\"), ("\n","\\n"), ("\r","\\r"), ("'","\\'"))
158 js_data
= js_data
.replace(item
[0], item
[1])
160 # Determine the correct path for VirtualHostMonsters & PCGI & etc.pp.
162 path
= "%s/misc_/Epoz/" % (self
.REQUEST
.get('BASEPATH1',''),)
165 pageurl
= self
.REQUEST
.get('URL1','')
168 i18n
='<script language="JavaScript" type="text/javascript" src="%(path)sepoz_multilingual.js?charset=%(charset)s" charset="%(charset)s"></script>' % {'path' : path
, 'charset' : charset
}
170 i18n
='<script language="JavaScript" type="text/javascript" src="%sepoz_lang_en.js"></script>' % (path
,)
172 i18n
+= '<script language="JavaScript" type="text/javascript" src="%sepoz_lang_%s.js"></script>' % (path
, lang
)
175 widget
= "%sepoz_script_widget.js" % path
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'
181 # Return the HTML-Code for the Epoz-Rich-Text-Editor
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">
191 InitIframe('%(name)s','%(js_data)s','%(path)s','%(toolbox)s','%(style)s','%(button)s','%(css)s','%(customcss)s','%(charset)s', '%(pageurl)s');
194 <noscript><textarea name="%(name)s" style="%(style)s">%(data)s</textarea></noscript>
205 'customcss': customcss
,
209 'iframesrc': iframesrc
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
223 input = html
.encode("utf-8")
224 input = EPOZ_SCRIPT
.sub('<script ', input)
225 input = input.replace('</epoz:script>', '</script>')
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")
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
)
249 output
= MSO_CLASS
.sub(r
"<\1>", output
)
250 result
= HTML_BODY
.search(output
)
252 output
= result
.group(1)
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
261 EpozPostTidy
= getattr(self
, 'EpozPostTidy', None)
262 if EpozPostTidy
is not None:
263 output
= EpozPostTidy(self
, output
, pageurl
)
265 return (errors
, output
, errordata
)
268 def EpozGetRelativeUrl(self
, pageUrl
, targetUrl
):
269 """ Returns relative path from targetUrl to pageUrl. """
271 # Just some shortcuts...
272 SCHEME
, HOSTNAME
, PATH
, PARAMS
, QUERY
, FRAGMENT
= range(6)
274 pageUnits
= urlparse
.urlparse(pageUrl
)
275 targetUnits
= urlparse
.urlparse(targetUrl
)
277 if pageUnits
[:PATH
] != targetUnits
[:PATH
]:
278 # Scheme (http) or host:port (www.comain.com) are different
279 # -> no possible relative URL
282 afterPath
= targetUnits
[PARAMS
:]
284 pagePath
= pageUnits
[PATH
].split('/')
287 targetPath
= targetUnits
[PATH
].split('/')
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]):
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
)
306 relativeUrl
= ['..'] * (len(pagePath
) -1)
307 relativeUrl
= ('', '', '/'.join(relativeUrl
+ targetPath
)) + tuple(afterPath
)
309 return urlparse
.urlunparse(relativeUrl
)
312 # Make sure the Epoz method is replaceable in the Products folder
313 Epoz
.__replaceable
__ = Globals
.REPLACEABLE
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
322 'Epoz__roles__': None,
324 'EpozTidy': EpozTidy
,
325 'EpozTidy__roles__': None,
327 'EpozGetRelativeUrl': EpozGetRelativeUrl
,
328 'EpozGetRelativeUrl__roles__': None,
330 'epoz_blank_iframe.html': PageTemplateFile('epoz/epoz_core/epoz_blank_iframe.html.pt', globals()),
331 'epoz_blank_iframe.html__roles__': None,
335 # And now try to register Epoz for CMF/Plone
338 from Products
.CMFCore
.DirectoryView
import registerDirectory
340 global cmfepoz_globals
341 cmfepoz_globals
=globals()
343 def initialize(context
):
344 registerDirectory('epoz', cmfepoz_globals
)