UI/Previewers/PreviewerHTML.py

changeset 3458
64bbac483843
child 3459
275cb00c83e2
diff -r bfc38662e6fc -r 64bbac483843 UI/Previewers/PreviewerHTML.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/UI/Previewers/PreviewerHTML.py	Mon Mar 31 19:27:31 2014 +0200
@@ -0,0 +1,431 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2014 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+
+"""
+
+import os
+import threading
+import re
+
+from PyQt4.QtCore import pyqtSlot, pyqtSignal, Qt, QUrl, QSize, QThread
+from PyQt4.QtGui import QWidget
+from PyQt4.QtWebKit import QWebPage
+
+from E5Gui.E5Application import e5App
+
+from .Ui_PreviewerHTML import Ui_PreviewerHTML
+
+import Utilities
+import Preferences
+
+
+class PreviewerHTML(QWidget, Ui_PreviewerHTML):
+    """
+    Class implementing a previewer widget for HTML, Markdown and ReST files.
+    """
+    def __init__(self, parent=None):
+        """
+        Constructor
+        
+        @param parent reference to the parent widget (QWidget)
+        """
+        super().__init__(parent)
+        self.setupUi(self)
+        
+        self.jsCheckBox.setChecked(
+            Preferences.getUI("ShowFilePreviewJS"))
+        self.ssiCheckBox.setChecked(
+            Preferences.getUI("ShowFilePreviewSSI"))
+        
+        self.previewView.page().setLinkDelegationPolicy(
+            QWebPage.DelegateAllLinks)
+        
+        self.__scrollBarPositions = {}
+        self.__vScrollBarAtEnd = {}
+        self.__hScrollBarAtEnd = {}
+        
+        self.__processingThread = PreviewProcessingThread()
+        self.__processingThread.htmlReady.connect(self.__setHtml)
+
+        self.__previewedPath = None
+        self.__previewedEditor = None
+    
+    def shutdown(self):
+        """
+        Public method to perform shutdown actions.
+        """
+        self.__processingThread.wait()
+    
+    @pyqtSlot(bool)
+    def on_jsCheckBox_clicked(self, checked):
+        """
+        Private slot to enable/disable JavaScript.
+        
+        @param checked state of the checkbox (boolean)
+        """
+        Preferences.setUI("ShowFilePreviewJS", checked)
+        self.__setJavaScriptEnabled(checked)
+    
+    def __setJavaScriptEnabled(self, enable):
+        """
+        Private method to enable/disable JavaScript.
+        
+        @param enable flag indicating the enable state (boolean)
+        """
+        self.jsCheckBox.setChecked(enable)
+        
+        settings = self.previewView.settings()
+        settings.setAttribute(settings.JavascriptEnabled, enable)
+        
+        self.processEditor()
+    
+    @pyqtSlot(bool)
+    def on_ssiCheckBox_clicked(self, checked):
+        """
+        Private slot to enable/disable SSI.
+        
+        @param checked state of the checkbox (boolean)
+        """
+        Preferences.setUI("ShowFilePreviewSSI", checked)
+        self.processEditor()
+    
+    def processEditor(self, editor=None):
+        """
+        Private slot to process an editor's text.
+        
+        @param editor editor to be processed (Editor)
+        """
+        if editor is None:
+            editor = self.__previewedEditor
+        else:
+            self.__previewedEditor = editor
+        
+        if editor is not None:
+            fn = editor.getFileName()
+            
+            if fn:
+                extension = os.path.normcase(os.path.splitext(fn)[1][1:])
+            else:
+                extension = ""
+            if extension in \
+                Preferences.getEditor("PreviewHtmlFileNameExtensions") or \
+               editor.getLanguage() == "HTML":
+                language = "HTML"
+            elif extension in \
+                    Preferences.getEditor("PreviewMarkdownFileNameExtensions"):
+                language = "Markdown"
+            elif extension in \
+                    Preferences.getEditor("PreviewRestFileNameExtensions"):
+                language = "ReST"
+            else:
+                self.__setHtml(fn, self.tr(
+                    "<p>No preview available for this type of file.</p>"))
+                return
+            
+            if fn:
+                project = e5App().getObject("Project")
+                if project.isProjectFile(fn):
+                    rootPath = project.getProjectPath()
+                else:
+                    rootPath = os.path.dirname(os.path.abspath(fn))
+            else:
+                rootPath = ""
+            
+            self.__processingThread.process(
+                fn, language, editor.text(),
+                self.ssiCheckBox.isChecked(), rootPath)
+
+    def __setHtml(self, filePath, html):
+        """
+        Private method to set the HTML to the view and restore the scroll bars
+        positions.
+        
+        @param filePath file path of the previewed editor (string)
+        @param html processed HTML text ready to be shown (string)
+        """
+        self.__saveScrollBarPositions()
+        self.__previewedPath = Utilities.normcasepath(
+            Utilities.fromNativeSeparators(filePath))
+        self.previewView.page().mainFrame().contentsSizeChanged.connect(
+            self.__restoreScrollBarPositions)
+        self.previewView.setHtml(html, baseUrl=QUrl.fromLocalFile(filePath))
+    
+    @pyqtSlot(str)
+    def on_previewView_titleChanged(self, title):
+        """
+        Private slot to handle a change of the title.
+        
+        @param title new title (string)
+        """
+        if title:
+            self.titleLabel.setText(self.tr("Preview - {0}").format(title))
+        else:
+            self.titleLabel.setText(self.tr("Preview"))
+    
+    def __saveScrollBarPositions(self):
+        """
+        Private method to save scroll bar positions for a previewed editor.
+        """
+        frame = self.previewView.page().mainFrame()
+        if frame.contentsSize() == QSize(0, 0):
+            return  # no valid data, nothing to save
+        
+        pos = frame.scrollPosition()
+        self.__scrollBarPositions[self.__previewedPath] = pos
+        self.__hScrollBarAtEnd[self.__previewedPath] = \
+            frame.scrollBarMaximum(Qt.Horizontal) == pos.x()
+        self.__vScrollBarAtEnd[self.__previewedPath] = \
+            frame.scrollBarMaximum(Qt.Vertical) == pos.y()
+
+    def __restoreScrollBarPositions(self):
+        """
+        Private method to restore scroll bar positions for a previewed editor.
+        """
+        try:
+            self.previewView.page().mainFrame().contentsSizeChanged.disconnect(
+                self.__restoreScrollBarPositions)
+        except TypeError:
+            # not connected, simply ignore it
+            pass
+        
+        if self.__previewedPath not in self.__scrollBarPositions:
+            return
+        
+        frame = self.previewView.page().mainFrame()
+        frame.setScrollPosition(
+            self.__scrollBarPositions[self.__previewedPath])
+        
+        if self.__hScrollBarAtEnd[self.__previewedPath]:
+            frame.setScrollBarValue(
+                Qt.Horizontal, frame.scrollBarMaximum(Qt.Horizontal))
+        
+        if self.__vScrollBarAtEnd[self.__previewedPath]:
+            frame.setScrollBarValue(
+                Qt.Vertical, frame.scrollBarMaximum(Qt.Vertical))
+    
+    @pyqtSlot(QUrl)
+    def on_previewView_linkClicked(self, url):
+        """
+        Private slot handling the clicking of a link.
+        
+        @param url url of the clicked link (QUrl)
+        """
+        e5App().getObject("UserInterface").launchHelpViewer(url.toString())
+
+
+class PreviewProcessingThread(QThread):
+    """
+    Class implementing a thread to process some text into HTML usable by the
+    previewer view.
+    
+    @signal htmlReady(str,str) emitted with the file name and processed HTML
+        to signal the availability of the processed HTML
+    """
+    htmlReady = pyqtSignal(str, str)
+    
+    def __init__(self, parent=None):
+        """
+        Constructor
+        
+        @param parent reference to the parent object (QObject)
+        """
+        super().__init__()
+        
+        self.__lock = threading.Lock()
+    
+    def process(self, filePath, language, text, ssiEnabled, rootPath):
+        """
+        Convert the given text to HTML.
+        
+        @param filePath file path of the text (string)
+        @param language language of the text (string)
+        @param text text to be processed (string)
+        @param ssiEnabled flag indicating to do some (limited) SSI processing
+            (boolean)
+        @param rootPath root path to be used for SSI processing (str)
+        """
+        with self.__lock:
+            self.__filePath = filePath
+            self.__language = language
+            self.__text = text
+            self.__ssiEnabled = ssiEnabled
+            self.__rootPath = rootPath
+            self.__haveData = True
+            if not self.isRunning():
+                self.start(QThread.LowPriority)
+    
+    def run(self):
+        """
+        Thread function to convert the stored data.
+        """
+        while True:
+            # exits with break
+            with self.__lock:
+                filePath = self.__filePath
+                language = self.__language
+                text = self.__text
+                ssiEnabled = self.__ssiEnabled
+                rootPath = self.__rootPath
+                self.__haveData = False
+            
+            html = self.__getHtml(language, text, ssiEnabled, filePath,
+                                  rootPath)
+            
+            with self.__lock:
+                if not self.__haveData:
+                    self.htmlReady.emit(filePath, html)
+                    break
+                # else - next iteration
+    
+    def __getHtml(self, language, text, ssiEnabled, filePath, rootPath):
+        """
+        Private method to process the given text depending upon the given
+        language.
+        
+        @param language language of the text (string)
+        @param text to be processed (string)
+        @param ssiEnabled flag indicating to do some (limited) SSI processing
+            (boolean)
+        @param filePath file path of the text (string)
+        @param rootPath root path to be used for SSI processing (str)
+        @return processed HTML text (string)
+        """
+        if language == "HTML":
+            if ssiEnabled:
+                return self.__processSSI(text, filePath, rootPath)
+            else:
+                return text
+        elif language == "Markdown":
+            return self.__convertMarkdown(text)
+        elif language == "ReST":
+            return self.__convertReST(text)
+        else:
+            return self.tr(
+                "<p>No preview available for this type of file.</p>")
+    
+    def __processSSI(self, txt, filename, root):
+        """
+        Private method to process the given text for SSI statements.
+        
+        Note: Only a limited subset of SSI statements are supported.
+        
+        @param txt text to be processed (string)
+        @param filename name of the file associated with the given text
+            (string)
+        @param root directory of the document root (string)
+        @return processed HTML (string)
+        """
+        if not filename:
+            return txt
+        
+        # SSI include
+        incRe = re.compile(
+            r"""<!--#include[ \t]+(virtual|file)=[\"']([^\"']+)[\"']\s*-->""",
+            re.IGNORECASE)
+        baseDir = os.path.dirname(os.path.abspath(filename))
+        docRoot = root if root != "" else baseDir
+        while True:
+            incMatch = incRe.search(txt)
+            if incMatch is None:
+                break
+            
+            if incMatch.group(1) == "virtual":
+                incFile = Utilities.normjoinpath(docRoot, incMatch.group(2))
+            elif incMatch.group(1) == "file":
+                incFile = Utilities.normjoinpath(baseDir, incMatch.group(2))
+            else:
+                incFile = ""
+            if os.path.exists(incFile):
+                try:
+                    f = open(incFile, "r")
+                    incTxt = f.read()
+                    f.close()
+                except (IOError, OSError):
+                    # remove SSI include
+                    incTxt = ""
+            else:
+                # remove SSI include
+                incTxt = ""
+            txt = txt[:incMatch.start(0)] + incTxt + txt[incMatch.end(0):]
+        
+        return txt
+    
+    def __convertReST(self, text):
+        """
+        Private method to convert ReST text into HTML.
+        
+        @param text text to be processed (string)
+        @return processed HTML (string)
+        """
+        try:
+            import docutils.core    # __IGNORE_EXCEPTION__ __IGNORE_WARNING__
+        except ImportError:
+            return self.tr(
+                """<p>ReStructuredText preview requires the"""
+                """ <b>python-docutils</b> package.<br/>Install it with"""
+                """ your package manager or see"""
+                """ <a href="http://pypi.python.org/pypi/docutils">"""
+                """this page.</a></p>""")
+        
+        return docutils.core.publish_string(text, writer_name='html')\
+            .decode("utf-8")
+    
+    def __convertMarkdown(self, text):
+        """
+        Private method to convert Markdown text into HTML.
+        
+        @param text text to be processed (string)
+        @return processed HTML (string)
+        """
+        try:
+            import markdown     # __IGNORE_EXCEPTION__ __IGNORE_WARNING__
+        except ImportError:
+            return self.tr(
+                """<p>Markdown preview requires the <b>python-markdown</b> """
+                """package.<br/>Install it with your package manager or see """
+                """<a href="http://pythonhosted.org/Markdown/install.html">"""
+                """installation instructions.</a></p>""")
+        
+        try:
+            import mdx_mathjax  # __IGNORE_EXCEPTION__ __IGNORE_WARNING__
+        except ImportError:
+            #mathjax doesn't require import statement if installed as extension
+            pass
+
+        extensions = ['fenced_code', 'nl2br', 'extra']
+        
+        # version 2.0 supports only extension names, not instances
+        if markdown.version_info[0] > 2 or \
+                (markdown.version_info[0] == 2 and
+                 markdown.version_info[1] > 0):
+            class _StrikeThroughExtension(markdown.Extension):
+                """
+                Class is placed here, because it depends on imported markdown,
+                and markdown import is lazy.
+                
+                (see http://achinghead.com/
+                python-markdown-adding-insert-delete.html this page for
+                details)
+                """
+                DEL_RE = r'(~~)(.*?)~~'
+
+                def extendMarkdown(self, md, md_globals):
+                    # Create the del pattern
+                    del_tag = markdown.inlinepatterns.SimpleTagPattern(
+                        self.DEL_RE, 'del')
+                    # Insert del pattern into markdown parser
+                    md.inlinePatterns.add('del', del_tag, '>not_strong')
+            
+            extensions.append(_StrikeThroughExtension())
+
+        try:
+            return markdown.markdown(text,  extensions + ['mathjax'])
+        except (ImportError, ValueError):
+            # markdown raises ValueError or ImportError, depends on version
+            # It is not clear, how to distinguish missing mathjax from other
+            # errors. So keep going without mathjax.
+            return markdown.markdown(text, extensions)

eric ide

mercurial