UI/Previewers/PreviewerHTML.py

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

eric ide

mercurial