UI/Previewers/PreviewerHTML.py

branch
Py2 comp.
changeset 3515
1b8381afe38f
parent 3459
275cb00c83e2
child 3539
0c2dc1446ebf
equal deleted inserted replaced
3506:d85fadb263a0 3515:1b8381afe38f
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2014 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing a previewer widget for HTML, Markdown and ReST files.
8 """
9
10 from __future__ import unicode_literals
11
12 import os
13 import threading
14 import re
15
16 from PyQt4.QtCore import pyqtSlot, pyqtSignal, Qt, QUrl, QSize, QThread
17 from PyQt4.QtGui import QWidget
18 from PyQt4.QtWebKit import QWebPage
19
20 from E5Gui.E5Application import e5App
21
22 from .Ui_PreviewerHTML import Ui_PreviewerHTML
23
24 import Utilities
25 import Preferences
26
27
28 class PreviewerHTML(QWidget, Ui_PreviewerHTML):
29 """
30 Class implementing a previewer widget for HTML, Markdown and ReST files.
31 """
32 def __init__(self, parent=None):
33 """
34 Constructor
35
36 @param parent reference to the parent widget (QWidget)
37 """
38 super(PreviewerHTML, self).__init__(parent)
39 self.setupUi(self)
40
41 self.jsCheckBox.setChecked(
42 Preferences.getUI("ShowFilePreviewJS"))
43 self.ssiCheckBox.setChecked(
44 Preferences.getUI("ShowFilePreviewSSI"))
45
46 self.previewView.page().setLinkDelegationPolicy(
47 QWebPage.DelegateAllLinks)
48
49 self.__scrollBarPositions = {}
50 self.__vScrollBarAtEnd = {}
51 self.__hScrollBarAtEnd = {}
52
53 self.__processingThread = PreviewProcessingThread()
54 self.__processingThread.htmlReady.connect(self.__setHtml)
55
56 self.__previewedPath = None
57 self.__previewedEditor = None
58
59 def shutdown(self):
60 """
61 Public method to perform shutdown actions.
62 """
63 self.__processingThread.wait()
64
65 @pyqtSlot(bool)
66 def on_jsCheckBox_clicked(self, checked):
67 """
68 Private slot to enable/disable JavaScript.
69
70 @param checked state of the checkbox (boolean)
71 """
72 Preferences.setUI("ShowFilePreviewJS", checked)
73 self.__setJavaScriptEnabled(checked)
74
75 def __setJavaScriptEnabled(self, enable):
76 """
77 Private method to enable/disable JavaScript.
78
79 @param enable flag indicating the enable state (boolean)
80 """
81 self.jsCheckBox.setChecked(enable)
82
83 settings = self.previewView.settings()
84 settings.setAttribute(settings.JavascriptEnabled, enable)
85
86 self.processEditor()
87
88 @pyqtSlot(bool)
89 def on_ssiCheckBox_clicked(self, checked):
90 """
91 Private slot to enable/disable SSI.
92
93 @param checked state of the checkbox (boolean)
94 """
95 Preferences.setUI("ShowFilePreviewSSI", checked)
96 self.processEditor()
97
98 def processEditor(self, editor=None):
99 """
100 Private slot to process an editor's text.
101
102 @param editor editor to be processed (Editor)
103 """
104 if editor is None:
105 editor = self.__previewedEditor
106 else:
107 self.__previewedEditor = editor
108
109 if editor is not None:
110 fn = editor.getFileName()
111
112 if fn:
113 extension = os.path.normcase(os.path.splitext(fn)[1][1:])
114 else:
115 extension = ""
116 if extension in \
117 Preferences.getEditor("PreviewHtmlFileNameExtensions") or \
118 editor.getLanguage() == "HTML":
119 language = "HTML"
120 elif extension in \
121 Preferences.getEditor("PreviewMarkdownFileNameExtensions"):
122 language = "Markdown"
123 elif extension in \
124 Preferences.getEditor("PreviewRestFileNameExtensions"):
125 language = "ReST"
126 else:
127 self.__setHtml(fn, self.tr(
128 "<p>No preview available for this type of file.</p>"))
129 return
130
131 if fn:
132 project = e5App().getObject("Project")
133 if project.isProjectFile(fn):
134 rootPath = project.getProjectPath()
135 else:
136 rootPath = os.path.dirname(os.path.abspath(fn))
137 else:
138 rootPath = ""
139
140 self.__processingThread.process(
141 fn, language, editor.text(),
142 self.ssiCheckBox.isChecked(), rootPath)
143
144 def __setHtml(self, filePath, html):
145 """
146 Private method to set the HTML to the view and restore the scroll bars
147 positions.
148
149 @param filePath file path of the previewed editor (string)
150 @param html processed HTML text ready to be shown (string)
151 """
152 self.__saveScrollBarPositions()
153 self.__previewedPath = Utilities.normcasepath(
154 Utilities.fromNativeSeparators(filePath))
155 self.previewView.page().mainFrame().contentsSizeChanged.connect(
156 self.__restoreScrollBarPositions)
157 self.previewView.setHtml(html, baseUrl=QUrl.fromLocalFile(filePath))
158
159 @pyqtSlot(str)
160 def on_previewView_titleChanged(self, title):
161 """
162 Private slot to handle a change of the title.
163
164 @param title new title (string)
165 """
166 if title:
167 self.titleLabel.setText(self.tr("Preview - {0}").format(title))
168 else:
169 self.titleLabel.setText(self.tr("Preview"))
170
171 def __saveScrollBarPositions(self):
172 """
173 Private method to save scroll bar positions for a previewed editor.
174 """
175 frame = self.previewView.page().mainFrame()
176 if frame.contentsSize() == QSize(0, 0):
177 return # no valid data, nothing to save
178
179 pos = frame.scrollPosition()
180 self.__scrollBarPositions[self.__previewedPath] = pos
181 self.__hScrollBarAtEnd[self.__previewedPath] = \
182 frame.scrollBarMaximum(Qt.Horizontal) == pos.x()
183 self.__vScrollBarAtEnd[self.__previewedPath] = \
184 frame.scrollBarMaximum(Qt.Vertical) == pos.y()
185
186 def __restoreScrollBarPositions(self):
187 """
188 Private method to restore scroll bar positions for a previewed editor.
189 """
190 try:
191 self.previewView.page().mainFrame().contentsSizeChanged.disconnect(
192 self.__restoreScrollBarPositions)
193 except TypeError:
194 # not connected, simply ignore it
195 pass
196
197 if self.__previewedPath not in self.__scrollBarPositions:
198 return
199
200 frame = self.previewView.page().mainFrame()
201 frame.setScrollPosition(
202 self.__scrollBarPositions[self.__previewedPath])
203
204 if self.__hScrollBarAtEnd[self.__previewedPath]:
205 frame.setScrollBarValue(
206 Qt.Horizontal, frame.scrollBarMaximum(Qt.Horizontal))
207
208 if self.__vScrollBarAtEnd[self.__previewedPath]:
209 frame.setScrollBarValue(
210 Qt.Vertical, frame.scrollBarMaximum(Qt.Vertical))
211
212 @pyqtSlot(QUrl)
213 def on_previewView_linkClicked(self, url):
214 """
215 Private slot handling the clicking of a link.
216
217 @param url url of the clicked link (QUrl)
218 """
219 e5App().getObject("UserInterface").launchHelpViewer(url.toString())
220
221
222 class PreviewProcessingThread(QThread):
223 """
224 Class implementing a thread to process some text into HTML usable by the
225 previewer view.
226
227 @signal htmlReady(str,str) emitted with the file name and processed HTML
228 to signal the availability of the processed HTML
229 """
230 htmlReady = pyqtSignal(str, str)
231
232 def __init__(self, parent=None):
233 """
234 Constructor
235
236 @param parent reference to the parent object (QObject)
237 """
238 super(PreviewProcessingThread, self).__init__()
239
240 self.__lock = threading.Lock()
241
242 def process(self, filePath, language, text, ssiEnabled, rootPath):
243 """
244 Convert the given text to HTML.
245
246 @param filePath file path of the text (string)
247 @param language language of the text (string)
248 @param text text to be processed (string)
249 @param ssiEnabled flag indicating to do some (limited) SSI processing
250 (boolean)
251 @param rootPath root path to be used for SSI processing (str)
252 """
253 with self.__lock:
254 self.__filePath = filePath
255 self.__language = language
256 self.__text = text
257 self.__ssiEnabled = ssiEnabled
258 self.__rootPath = rootPath
259 self.__haveData = True
260 if not self.isRunning():
261 self.start(QThread.LowPriority)
262
263 def run(self):
264 """
265 Thread function to convert the stored data.
266 """
267 while True:
268 # exits with break
269 with self.__lock:
270 filePath = self.__filePath
271 language = self.__language
272 text = self.__text
273 ssiEnabled = self.__ssiEnabled
274 rootPath = self.__rootPath
275 self.__haveData = False
276
277 html = self.__getHtml(language, text, ssiEnabled, filePath,
278 rootPath)
279
280 with self.__lock:
281 if not self.__haveData:
282 self.htmlReady.emit(filePath, html)
283 break
284 # else - next iteration
285
286 def __getHtml(self, language, text, ssiEnabled, filePath, rootPath):
287 """
288 Private method to process the given text depending upon the given
289 language.
290
291 @param language language of the text (string)
292 @param text to be processed (string)
293 @param ssiEnabled flag indicating to do some (limited) SSI processing
294 (boolean)
295 @param filePath file path of the text (string)
296 @param rootPath root path to be used for SSI processing (str)
297 @return processed HTML text (string)
298 """
299 if language == "HTML":
300 if ssiEnabled:
301 return self.__processSSI(text, filePath, rootPath)
302 else:
303 return text
304 elif language == "Markdown":
305 return self.__convertMarkdown(text)
306 elif language == "ReST":
307 return self.__convertReST(text)
308 else:
309 return self.tr(
310 "<p>No preview available for this type of file.</p>")
311
312 def __processSSI(self, txt, filename, root):
313 """
314 Private method to process the given text for SSI statements.
315
316 Note: Only a limited subset of SSI statements are supported.
317
318 @param txt text to be processed (string)
319 @param filename name of the file associated with the given text
320 (string)
321 @param root directory of the document root (string)
322 @return processed HTML (string)
323 """
324 if not filename:
325 return txt
326
327 # SSI include
328 incRe = re.compile(
329 r"""<!--#include[ \t]+(virtual|file)=[\"']([^\"']+)[\"']\s*-->""",
330 re.IGNORECASE)
331 baseDir = os.path.dirname(os.path.abspath(filename))
332 docRoot = root if root != "" else baseDir
333 while True:
334 incMatch = incRe.search(txt)
335 if incMatch is None:
336 break
337
338 if incMatch.group(1) == "virtual":
339 incFile = Utilities.normjoinpath(docRoot, incMatch.group(2))
340 elif incMatch.group(1) == "file":
341 incFile = Utilities.normjoinpath(baseDir, incMatch.group(2))
342 else:
343 incFile = ""
344 if os.path.exists(incFile):
345 try:
346 f = open(incFile, "r")
347 incTxt = f.read()
348 f.close()
349 except (IOError, OSError):
350 # remove SSI include
351 incTxt = ""
352 else:
353 # remove SSI include
354 incTxt = ""
355 txt = txt[:incMatch.start(0)] + incTxt + txt[incMatch.end(0):]
356
357 return txt
358
359 def __convertReST(self, text):
360 """
361 Private method to convert ReST text into HTML.
362
363 @param text text to be processed (string)
364 @return processed HTML (string)
365 """
366 try:
367 import docutils.core # __IGNORE_EXCEPTION__ __IGNORE_WARNING__
368 except ImportError:
369 return self.tr(
370 """<p>ReStructuredText preview requires the"""
371 """ <b>python-docutils</b> package.<br/>Install it with"""
372 """ your package manager or see"""
373 """ <a href="http://pypi.python.org/pypi/docutils">"""
374 """this page.</a></p>""")
375
376 return docutils.core.publish_string(text, writer_name='html')\
377 .decode("utf-8")
378
379 def __convertMarkdown(self, text):
380 """
381 Private method to convert Markdown text into HTML.
382
383 @param text text to be processed (string)
384 @return processed HTML (string)
385 """
386 try:
387 import markdown # __IGNORE_EXCEPTION__ __IGNORE_WARNING__
388 except ImportError:
389 return self.tr(
390 """<p>Markdown preview requires the <b>python-markdown</b> """
391 """package.<br/>Install it with your package manager or see """
392 """<a href="http://pythonhosted.org/Markdown/install.html">"""
393 """installation instructions.</a></p>""")
394
395 try:
396 import mdx_mathjax # __IGNORE_EXCEPTION__ __IGNORE_WARNING__
397 except ImportError:
398 #mathjax doesn't require import statement if installed as extension
399 pass
400
401 extensions = ['fenced_code', 'nl2br', 'extra']
402
403 # version 2.0 supports only extension names, not instances
404 if markdown.version_info[0] > 2 or \
405 (markdown.version_info[0] == 2 and
406 markdown.version_info[1] > 0):
407 class _StrikeThroughExtension(markdown.Extension):
408 """
409 Class is placed here, because it depends on imported markdown,
410 and markdown import is lazy.
411
412 (see http://achinghead.com/
413 python-markdown-adding-insert-delete.html this page for
414 details)
415 """
416 DEL_RE = r'(~~)(.*?)~~'
417
418 def extendMarkdown(self, md, md_globals):
419 # Create the del pattern
420 del_tag = markdown.inlinepatterns.SimpleTagPattern(
421 self.DEL_RE, 'del')
422 # Insert del pattern into markdown parser
423 md.inlinePatterns.add('del', del_tag, '>not_strong')
424
425 extensions.append(_StrikeThroughExtension())
426
427 try:
428 return markdown.markdown(text, extensions + ['mathjax'])
429 except (ImportError, ValueError):
430 # markdown raises ValueError or ImportError, depends on version
431 # It is not clear, how to distinguish missing mathjax from other
432 # errors. So keep going without mathjax.
433 return markdown.markdown(text, extensions)

eric ide

mercurial