def test_16(self): for _ in self.singleThreadOnly: rl = RunLatest(_, True) # Start a job. q1a = Queue() q1b = Queue() def f1(): q1b.put(None) q1a.get() em1 = Emitter('em1 should never be called by {}'.format(_), self.assertEqual) future1 = rl.start(em1.g, f1) q1b.get() self.assertEqual(future1.state, Future.STATE_RUNNING) # Start another job, canceling the previous job while it's running. em2 = Emitter() rl.start(em2.g, lambda: None) with WaitForSignal(em2.bing, 1000): q1a.put(None) rl.terminate()
def test_15(self): for _ in self.singleThreadOnly: rl = RunLatest(_) # Start a job, keeping it running. q1a = Queue() q1b = Queue() def f1(): q1b.put(None) q1a.get() em1 = Emitter() future1 = rl.start(em1.g, f1) q1b.get() self.assertEqual(future1.state, Future.STATE_RUNNING) # Start two more. The first should not run; if it does, it raises # an exception. def f2(): raise TypeError rl.start(None, f2) em3 = Emitter() rl.start(em3.g, lambda: None) with WaitForSignal(em3.bing, 1000): q1a.put(None) rl.terminate()
def test_15(self): for _ in self.singleThreadOnly: rl = RunLatest(_) # Start a job, keeping it running. q1a = Queue() q1b = Queue() def f1(): q1b.put(None) q1a.get() em1 = Emitter() future1 = rl.start(em1.g, f1) q1b.get() self.assertEqual(future1.state, Future.STATE_RUNNING) # Start two more. The first should not run; if it does, it raises # an exception. def f2(): raise TypeError rl.start(None, f2) em3 = Emitter() rl.start(em3.g, lambda: None) with WaitForSignal(em3.bing, 1000): q1a.put(None) rl.terminate()
def test_16(self): for _ in self.singleThreadOnly: rl = RunLatest(_, True) # Start a job. q1a = Queue() q1b = Queue() def f1(): q1b.put(None) q1a.get() em1 = Emitter('em1 should never be called by {}'.format(_), self.assertEqual) future1 = rl.start(em1.g, f1) q1b.get() self.assertEqual(future1.state, Future.STATE_RUNNING) # Start another job, canceling the previous job while it's running. em2 = Emitter() rl.start(em2.g, lambda: None) with WaitForSignal(em2.bing, 1000): q1a.put(None) rl.terminate()
class PreviewDock(DockWidget): """GUI and implementation """ # Emitted when this window is closed. closed = pyqtSignal() def __init__(self): DockWidget.__init__(self, core.mainWindow(), "Previe&w", QIcon(':/enkiicons/internet.png'), "Alt+W") self._widget = self._createWidget() # Don't need to schedule document processing; a call to show() does. self._loadTemplates() self._widget.cbTemplate.currentIndexChanged.connect( self._onCurrentTemplateChanged) # Disconnected. # When quitting this program, don't rebuild when closing all open # documents. This can take a long time, particularly if a some of the # documents are associated with a Sphinx project. self._programRunning = True core.aboutToTerminate.connect( self._quitingApplication) # Disconnected. core.workspace().currentDocumentChanged.connect( self._onDocumentChanged) # Disconnected. core.workspace().textChanged.connect( self._onTextChanged) # Disconnected. # If the user presses the accept button in the setting dialog, Enki # will force a rebuild of the whole project. # # TODO: only build if preview settings have been changed. # # In order to make this happen, let ``_onSettingsDialogAboutToExecute`` emit # a signal indicating that the CodeChat setting dialog has been opened. Save # core.config()['Sphinx'] and core.config()['CodeChat']. After dialogAccepted # is detected, compare current settings with the old one. Build if necessary. core.uiSettingsManager().dialogAccepted.connect( self._scheduleDocumentProcessing) # Disconnected. core.workspace().modificationChanged.connect( self._onDocumentModificationChanged) # disconnected self._scrollPos = {} self._vAtEnd = {} self._hAtEnd = {} # Keep track of which Sphinx template copies we've already asked the user about. self._sphinxTemplateCheckIgnoreList = [] self._sphinxConverter = SphinxConverter(self) # stopped self._runLatest = RunLatest('QThread', parent=self) self._visiblePath = None # If we update Preview on every key press, freezes are noticable (the # GUI thread draws the preview too slowly). # This timer is used for drawing Preview 800 ms After user has stopped typing text self._typingTimer = QTimer() # stopped. self._typingTimer.setInterval(800) self._typingTimer.timeout.connect( self._scheduleDocumentProcessing) # Disconnected. self.previewSync = PreviewSync(self) # del_ called self._applyJavaScriptEnabled(self._isJavaScriptEnabled()) # Clear flags used to temporarily disable signals during # ``_scheduleDocumentProcessing.``. self._ignoreDocumentChanged = False self._ignoreTextChanges = False # Provide an inital value for the rebuild needed flag. self._rebuildNeeded = False # Save the initial font, then restore it after a ``clear``. Note that # ``clear()`` doesn't reset the `currentCharFormat # <http://doc.qt.io/qt-4.8/qplaintextedit.html#currentCharFormat>`_. In # fact, clicking in red (error/warning) message in the log window # changes the current font to red! So, save it here so that it will be # restored correctly on a ``_clear_log``. self._defaultLogFont = self._widget.teLog.currentCharFormat() # The logWindowClear signal clears the log window. self._sphinxConverter.logWindowClear.connect( self._clear_log) # disconnected # The logWindowText signal simply appends text to the log window. self._sphinxConverter.logWindowText.connect( lambda s: self._widget.teLog.appendPlainText(s)) # disconnected def _createWidget(self): widget = QWidget(self) uic.loadUi(os.path.join(os.path.dirname(__file__), 'Preview.ui'), widget) widget.layout().setContentsMargins(0, 0, 0, 0) widget.webView.page().setLinkDelegationPolicy( QWebPage.DelegateAllLinks) widget.webView.page().linkClicked.connect( self._onLinkClicked) # Disconnected. # Fix preview palette. See https://github.com/bjones1/enki/issues/34 webViewPalette = widget.webView.palette() webViewPalette.setColor(QPalette.Inactive, QPalette.HighlightedText, webViewPalette.color(QPalette.Text)) widget.webView.setPalette(webViewPalette) widget.webView.page().mainFrame().titleChanged.connect( self._updateTitle) # Disconnected. widget.cbEnableJavascript.clicked.connect( self._onJavaScriptEnabledCheckbox) # Disconnected. widget.webView.installEventFilter(self) self.setWidget(widget) self.setFocusProxy(widget.webView) widget.tbSave.clicked.connect(self.onPreviewSave) # Disconnected. # Add an attribute to ``widget`` denoting the splitter location. # This value will be overwritten when the user changes splitter location. widget.splitterErrorStateSize = (199, 50) widget.splitterNormStateSize = (1, 0) widget.splitterNormState = True widget.splitter.setSizes(widget.splitterNormStateSize) widget.splitter.splitterMoved.connect( self.on_splitterMoved) # Disconnected. return widget def _quitingApplication(self): self._programRunning = False def on_splitterMoved(self, pos, index): if self._widget.splitterNormState: self._widget.splitterNormStateSize = self._widget.splitter.sizes() else: self._widget.splitterErrorStateSize = self._widget.splitter.sizes() def terminate(self): """Uninstall themselves """ self._typingTimer.stop() self._typingTimer.timeout.disconnect(self._scheduleDocumentProcessing) try: self._widget.webView.page().mainFrame().loadFinished.disconnect( self._restoreScrollPos) except TypeError: # already has been disconnected pass self.previewSync.terminate() core.workspace().modificationChanged.disconnect( self._onDocumentModificationChanged) self._widget.cbTemplate.currentIndexChanged.disconnect( self._onCurrentTemplateChanged) core.aboutToTerminate.disconnect(self._quitingApplication) core.workspace().currentDocumentChanged.disconnect( self._onDocumentChanged) core.workspace().textChanged.disconnect(self._onTextChanged) core.uiSettingsManager().dialogAccepted.disconnect( self._scheduleDocumentProcessing) self._widget.webView.page().linkClicked.disconnect(self._onLinkClicked) self._widget.webView.page().mainFrame().titleChanged.disconnect( self._updateTitle) self._widget.cbEnableJavascript.clicked.disconnect( self._onJavaScriptEnabledCheckbox) self._widget.tbSave.clicked.disconnect(self.onPreviewSave) self._widget.splitter.splitterMoved.disconnect(self.on_splitterMoved) self._sphinxConverter.logWindowClear.disconnect(self._clear_log) self._sphinxConverter.logWindowText.disconnect() self._sphinxConverter.terminate() self._runLatest.terminate() def closeEvent(self, event): """Widget is closed. Clear it """ self.closed.emit() self._clear() return DockWidget.closeEvent(self, event) def _clear_log(self): """Clear the log window and reset the default font.""" self._widget.teLog.clear() self._widget.teLog.setCurrentCharFormat(self._defaultLogFont) def eventFilter(self, obj, ev): """Event filter for the web view Zooms the web view """ if isinstance(ev, QWheelEvent) and \ ev.modifiers() == Qt.ControlModifier: multiplier = 1 + (0.1 * (ev.angleDelta().y() / 120.)) view = self._widget.webView view.setZoomFactor(view.zoomFactor() * multiplier) return True else: return DockWidget.eventFilter(self, obj, ev) def _onDocumentModificationChanged(self, document, modified): if not modified: # probably has been saved just now if not self._ignoreDocumentChanged: self._scheduleDocumentProcessing() def _onLinkClicked(self, url): res = QDesktopServices.openUrl(url) if res: core.mainWindow().statusBar().showMessage( "{} opened in a browser".format(url.toString()), 2000) else: core.mainWindow().statusBar().showMessage( "Failed to open {}".format(url.toString()), 2000) def _updateTitle(self, pageTitle): """Web page title changed. Update own title. """ if pageTitle: self.setWindowTitle("Previe&w - " + pageTitle) else: self.setWindowTitle("Previe&w") def _saveScrollPos(self): """Save scroll bar position for document """ frame = self._widget.webView.page().mainFrame() if frame.contentsSize() == QSize(0, 0): return # no valida data, nothing to save pos = frame.scrollPosition() self._scrollPos[self._visiblePath] = pos self._hAtEnd[self._visiblePath] = frame.scrollBarMaximum( Qt.Horizontal) == pos.x() self._vAtEnd[self._visiblePath] = frame.scrollBarMaximum( Qt.Vertical) == pos.y() def _restoreScrollPos(self, ok): """Restore scroll bar position for document """ try: self._widget.webView.page().mainFrame().loadFinished.disconnect( self._restoreScrollPos) except TypeError: # already has been disconnected pass if core.workspace().currentDocument() is None: return # nothing to restore if don't have document if not self._visiblePath in self._scrollPos: return # no data for this document # Don't restore the scroll position if the window is hidden. This can # happen when the current document is changed, which invokes _clear, # which calls setHtml, which calls _saveScrollPos and then this routine # when the HTML is loaded. if not self.isVisible(): return frame = self._widget.webView.page().mainFrame() frame.setScrollPosition(self._scrollPos[self._visiblePath]) if self._hAtEnd[self._visiblePath]: frame.setScrollBarValue(Qt.Horizontal, frame.scrollBarMaximum(Qt.Horizontal)) if self._vAtEnd[self._visiblePath]: frame.setScrollBarValue(Qt.Vertical, frame.scrollBarMaximum(Qt.Vertical)) # Re-sync the re-loaded text. self.previewSync.syncTextToPreview() def _onDocumentChanged(self, old, new): """Current document changed, update preview """ self._typingTimer.stop() if new is not None: if new.qutepart.language() == 'Markdown': self._widget.cbTemplate.show() self._widget.lTemplate.show() else: self._widget.cbTemplate.hide() self._widget.lTemplate.hide() self._clear() if self.isVisible(): self._scheduleDocumentProcessing() _CUSTOM_TEMPLATE_PATH = '<custom template>' def _loadTemplates(self): for path in [ os.path.join(os.path.dirname(__file__), 'templates'), os.path.expanduser('~/.enki/markdown-templates') ]: if os.path.isdir(path): for fileName in os.listdir(path): fullPath = os.path.join(path, fileName) if os.path.isfile(fullPath): self._widget.cbTemplate.addItem(fileName, fullPath) self._widget.cbTemplate.addItem('Custom...', self._CUSTOM_TEMPLATE_PATH) self._restorePreviousTemplate() def _restorePreviousTemplate(self): # restore previous template index = self._widget.cbTemplate.findText( core.config()['Preview']['Template']) if index != -1: self._widget.cbTemplate.setCurrentIndex(index) def _getCurrentTemplatePath(self): index = self._widget.cbTemplate.currentIndex() if index == -1: # empty combo return '' return str(self._widget.cbTemplate.itemData(index)) def _getCurrentTemplate(self): path = self._getCurrentTemplatePath() if not path: return '' try: with open(path) as file: text = file.read() except Exception as ex: text = 'Failed to load template {}: {}'.format(path, ex) core.mainWindow().statusBar().showMessage(text) return '' else: return text def _onCurrentTemplateChanged(self): """Update text or show message to the user""" if self._getCurrentTemplatePath() == self._CUSTOM_TEMPLATE_PATH: QMessageBox.information( core.mainWindow(), 'Custom templaes help', '<html>See <a href="https://github.com/hlamer/enki/wiki/Markdown-preview-templates">' 'this</a> wiki page for information about custom templates') self._restorePreviousTemplate() core.config( )['Preview']['Template'] = self._widget.cbTemplate.currentText() core.config().flush() self._scheduleDocumentProcessing() def _onTextChanged(self, document): """Text changed, update preview """ if self.isVisible() and not self._ignoreTextChanges: self._typingTimer.stop() self._typingTimer.start() def show(self): """When shown, update document, if possible. """ DockWidget.show(self) self._scheduleDocumentProcessing() def _clear(self): """Clear the preview dock contents. Might be necesssary for stop executing JS and loading data. """ self._setHtml('', '', None, QUrl()) def _isJavaScriptEnabled(self): """Check if JS is enabled in the settings. """ return core.config()['Preview']['JavaScriptEnabled'] def _onJavaScriptEnabledCheckbox(self, enabled): """Checkbox clicked, save and apply settings """ core.config()['Preview']['JavaScriptEnabled'] = enabled core.config().flush() self._applyJavaScriptEnabled(enabled) def _applyJavaScriptEnabled(self, enabled): """Update QWebView settings and QCheckBox state """ self._widget.cbEnableJavascript.setChecked(enabled) settings = self._widget.webView.settings() settings.setAttribute(settings.JavascriptEnabled, enabled) def onPreviewSave(self): """Save contents of the preview pane to a user-specified file.""" path, _ = QFileDialog.getSaveFileName(self, 'Save Preview as HTML', filter='HTML (*.html)') if path: self._previewSave(path) def _previewSave(self, path): """Save contents of the preview pane to the file given by path.""" text = self._widget.webView.page().mainFrame().toHtml() try: with open(path, 'w', encoding='utf-8') as openedFile: openedFile.write(text) except (OSError, IOError) as ex: QMessageBox.critical(self, "Failed to save HTML", str(ex)) # HTML generation #---------------- # The following methods all support generation of HTML from text in the # Qutepart window in a separate thread. def _scheduleDocumentProcessing(self): """Start document processing with the thread. """ if not self._programRunning: return if self.isHidden(): return self._typingTimer.stop() document = core.workspace().currentDocument() if document is not None: if sphinxEnabledForFile(document.filePath()): self._copySphinxProjectTemplate(document.filePath()) qp = document.qutepart language = qp.language() text = qp.text sphinxCanProcess = sphinxEnabledForFile(document.filePath()) # Determine if we're in the middle of a build. currentlyBuilding = self._widget.prgStatus.text() == 'Building...' if language == 'Markdown': text = self._getCurrentTemplate() + text # Hide the progress bar, since processing is usually short and # Markdown produces no errors or warnings to display in the # progress bar. See https://github.com/bjones1/enki/issues/36. self._widget.prgStatus.setVisible(False) # Hide the error log, since Markdown never generates errors or # warnings. self._widget.teLog.setVisible(False) elif isHtmlFile(document): # No processing needed -- just display it. self._setHtml(document.filePath(), text, None, QUrl()) # Hide the progress bar, since no processing is necessary. self._widget.prgStatus.setVisible(False) # Hide the error log, since we do not HTML checking. self._widget.teLog.setVisible(False) return elif ((language == 'reStructuredText') or sphinxCanProcess or canUseCodeChat(document.filePath())): # Show the progress bar and error log for reST, CodeChat, or # Sphinx builds. It will display progress (Sphinx only) and # errors/warnings (for all three). self._widget.prgStatus.setVisible(True) self._widget.teLog.setVisible(True) self._setHtmlProgress('Building...') # Determine whether to initiate a build or not. The underlying # logic: # # - If Sphinx can't process this file, just build it. # - If Sphinx can process this file: # # - If the document isn't internally modified, we're here because # the file was saved or the refresh button was pressed. Build it. # - If the document was internally modified and "insta-build" is # enabled (i.e. build only on save is disabled): # # - If the document was not externally modified, then save and # build. # - If the document was externally modified, DANGER! The user # needs to decide which file wins (external changes or # internal changes). Don't save and build, since this would # overwrite external modifications without the user realizing # what happened. Instead, warn the user. # # As a table, see below. Build, Save, and Warn are the outputs; all # others are inputs. # # ================== =================== =================== ============= ===== ==== ==== # Sphinx can process Internally modified Externally modified Build on Save Build Save Warn # ================== =================== =================== ============= ===== ==== ==== # No X X X Yes No No # Yes No X X Yes No No # Yes Yes No No Yes Yes No # Yes Yes Yes No No No Yes # Yes Yes X Yes No No No # ================== =================== =================== ============= ===== ==== ==== internallyModified = qp.document().isModified() externallyModified = document.isExternallyModified() buildOnSave = core.config()['Sphinx']['BuildOnSave'] saveThenBuild = (sphinxCanProcess and internallyModified and not externallyModified and not buildOnSave) # If Sphinx is currently building, don't autosave -- this can # cause Sphinx to miss changes on its next build. Instead, wait # until Sphinx completes, then do a save and build. if saveThenBuild and currentlyBuilding: self._rebuildNeeded = True saveThenBuild = False else: self._rebuildNeeded = False # Save first, if needed. if saveThenBuild: # If trailing whitespace strip changes the cursor position, # restore the whitespace and cursor position. lineNum, col = qp.cursorPosition lineText = qp.lines[lineNum] # Invoking saveFile when Strip Trailing whitespace is enabled # causes ``onTextChanged`` (due to whitespace strips) and # ``onDocumentChanged`` signals to be emitted. These both # re-invoke this routine, causing a double build. So, ignore # both these signals. self._ignoreDocumentChanged = True self._ignoreTextChanges = True document.saveFile() self._ignoreDocumentChanged = False self._ignoreTextChanges = False if qp.cursorPosition != (lineNum, col): # Mark this as one operation on the undo stack. To do so, # enclose all editing operations in a context manager. See # "Text modification and Undo/Redo" in the qutepart docs. with qp: qp.lines[lineNum] = lineText qp.cursorPosition = lineNum, col qp.document().setModified(False) # Build. Each line is one row in the table above. if ((not sphinxCanProcess) or (sphinxCanProcess and not internallyModified) or saveThenBuild): # Build the HTML in a separate thread. self._runLatest.start(self._setHtmlFuture, self.getHtml, language, text, document.filePath()) # Warn. if (sphinxCanProcess and internallyModified and externallyModified and not buildOnSave): core.mainWindow().appendMessage( 'Warning: file modified externally. Auto-save disabled.') def getHtml(self, language, text, filePath): """Get HTML for document. This is run in a separate thread. """ if language == 'Markdown': return filePath, _convertMarkdown(text), None, QUrl() # For ReST, use docutils only if Sphinx isn't available. elif language == 'reStructuredText' and not sphinxEnabledForFile( filePath): htmlUnicode, errString = _convertReST(text) return filePath, htmlUnicode, errString, QUrl() elif filePath and sphinxEnabledForFile( filePath): # Use Sphinx to generate the HTML if possible. return self._sphinxConverter.convert(filePath) elif filePath and canUseCodeChat( filePath): # Otherwise, fall back to using CodeChat+docutils. return _convertCodeChat(text, filePath) else: return filePath, 'No preview for this type of file', None, QUrl() def _copySphinxProjectTemplate(self, documentFilePath): """Add conf.py, CodeChat.css and index.rst (if ther're missing) to the Sphinx project directory. """ if core.config( )['Sphinx']['ProjectPath'] in self._sphinxTemplateCheckIgnoreList: return # Check for the existance Sphinx project files. Copy skeleton versions # of them to the project if necessary. sphinxPluginsPath = os.path.dirname(os.path.realpath(__file__)) sphinxTemplatePath = os.path.join(sphinxPluginsPath, 'sphinx_templates') sphinxProjectPath = core.config()['Sphinx']['ProjectPath'] errors = [] checklist = ['index.rst', 'conf.py'] if core.config()['CodeChat']['Enabled'] and CodeChat: checklist.append('CodeChat.css') missinglist = [] for filename in checklist: if not os.path.exists(os.path.join(sphinxProjectPath, filename)): missinglist.append(filename) if not missinglist: return errors # For testing, check for test-provided button presses if ((len(self._sphinxTemplateCheckIgnoreList) == 1) and isinstance(self._sphinxTemplateCheckIgnoreList[0], int)): res = self._sphinxTemplateCheckIgnoreList[0] else: res = QMessageBox.warning( self, r"Enki", "Sphinx project at:\n " + sphinxProjectPath + "\nis missing the template file(s): " + ' '.join(missinglist) + ". Auto-generate those file(s)?", QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel, QMessageBox.Yes) if res != QMessageBox.Yes: if res == QMessageBox.No: self._sphinxTemplateCheckIgnoreList.append(sphinxProjectPath) return if core.config()['CodeChat']['Enabled'] and CodeChat: codeChatPluginsPath = os.path.dirname( os.path.realpath(CodeChat.__file__)) codeChatTemplatePath = os.path.join(codeChatPluginsPath, 'template') copyTemplateFile(errors, codeChatTemplatePath, 'index.rst', sphinxProjectPath) copyTemplateFile(errors, codeChatTemplatePath, 'conf.py', sphinxProjectPath) copyTemplateFile(errors, codeChatTemplatePath, 'CodeChat.css', sphinxProjectPath) else: copyTemplateFile(errors, sphinxTemplatePath, 'index.rst', sphinxProjectPath) copyTemplateFile(errors, sphinxTemplatePath, 'conf.py', sphinxProjectPath) errInfo = "" for error in errors: errInfo += "Copy from " + error[0] + " to " + error[ 1] + " caused error " + error[2] + ';\n' if errInfo: QMessageBox.warning( self, "Sphinx template file copy error", "Copy template project files failed. The following errors are returned:<br>" + errInfo) return errors def _setHtmlFuture(self, future): """Receives a future and unpacks the result, calling _setHtml.""" filePath, htmlText, errString, baseUrl = future.result self._setHtml(filePath, htmlText, errString, baseUrl) def _setHtml(self, filePath, htmlText, errString, baseUrl): """Set HTML to the view and restore scroll bars position. Called by the thread """ self._saveScrollPos() self._visiblePath = filePath self._widget.webView.page().mainFrame().loadFinished.connect( self._restoreScrollPos) # disconnected if baseUrl.isEmpty(): # Clear the log, then update it with build content. self._widget.teLog.clear() self._widget.webView.setHtml(htmlText, baseUrl=QUrl.fromLocalFile(filePath)) else: self._widget.webView.setUrl(baseUrl) # If there were messages from the conversion process, extract a count of # errors and warnings from these messages. if errString: # If there are errors/warnings, expand log window to make it visible if self._widget.splitterNormState: self._widget.splitterNormStateSize = self._widget.splitter.sizes( ) self._widget.splitterNormState = False self._widget.splitter.setSizes(self._widget.splitterErrorStateSize) # This code parses the error string to determine get the number of # warnings and errors. Common docutils error messages read:: # # <string>:1589: (ERROR/3) Unknown interpreted text role "ref". # # X:\ode.py:docstring of sympy:5: (ERROR/3) Unexpected indentation. # # and common sphinx errors read:: # # X:\SVM_train.m.rst:2: SEVERE: Title overline & underline mismatch. # # X:\indexs.rst:None: WARNING: image file not readable: a.jpg # # X:\conf.py.rst:: WARNING: document isn't included in any toctree # # Each error/warning occupies one line. The following `regular # expression # <https://docs.python.org/2/library/re.html#regular-expression-syntax>`_ # is designed to find the error position (1589/None) and message # type (ERROR/WARNING/SEVERE). Extra spaces are added to show which # parts of the example string it matches. For more details about # Python regular expressions, refer to the # `re docs <https://docs.python.org/2/library/re.html>`_. # # Examining this expression one element at a time:: # # <string>:1589: (ERROR/3)Unknown interpreted text role "ref". errPosRe = ':(\d*|None): ' # Find the first occurence of a pair of colons. # Between them there can be numbers or "None" or nothing. For example, # this expression matches the string ":1589:" or string ":None:" or # string "::". Next:: # # <string>:1589: (ERROR/3)Unknown interpreted text role "ref". errTypeRe = '\(?(WARNING|ERROR|SEVERE)' # Next match the error type, which can # only be "WARNING", "ERROR" or "SEVERE". Before this error type the # message may optionally contain one left parenthesis. # errEolRe = '.*$' # Since one error message occupies one line, a ``*`` # quantifier is used along with end-of-line ``$`` to make sure only # the first match is used in each line. # # TODO: Is this necesary? Is there any case where omitting this # causes a failure? regex = re.compile( errPosRe + errTypeRe + errEolRe, # The message usually contain multiple lines; search each line # for errors and warnings. re.MULTILINE) # Use findall to return all matches in the message, not just the # first. result = regex.findall(errString) # The variable ``result`` now contains a list of tuples, where each # tuples contains the two matched groups (line number, error_string). # For example:: # # [('1589', 'ERROR')] # # Therefeore, the second element of each tuple, represented as x[1], # is the error_string. The next two lines of code will collect all # ERRORs/SEVEREs and WARNINGs found in the error_string separately. errNum = sum([x[1] == 'ERROR' or x[1] == 'SEVERE' for x in result]) warningNum = [x[1] for x in result].count('WARNING') # Report these results this to the user. status = 'Error(s): {}, warning(s): {}'.format(errNum, warningNum) # Since the error string might contain characters such as ">" and "<", # they need to be converted to ">" and "<" such that # they can be displayed correctly in the log window as HTML strings. # This step is handled by ``html.escape``. self._widget.teLog.appendHtml("<pre><font color='red'>\n" + html.escape(errString) + '</font></pre>') # Update the progress bar. color = 'red' if errNum else '#FF9955' if warningNum else None self._setHtmlProgress(status, color) else: # If there are no errors/warnings, collapse the log window (can mannually # expand it back to visible) if not self._widget.splitterNormState: self._widget.splitterErrorStateSize = self._widget.splitter.sizes( ) self._widget.splitterNormState = True self._widget.splitter.setSizes(self._widget.splitterNormStateSize) self._setHtmlProgress('Error(s): 0, warning(s): 0') # Do a rebuild if needed. if self._rebuildNeeded: self._rebuildNeeded = False self._scheduleDocumentProcessing() def _setHtmlProgress(self, text, color=None): """Set progress label. """ if color: style = 'QLabel { background-color: ' + color + '; }' else: style = style = 'QLabel {}' self._widget.prgStatus.setStyleSheet(style) self._widget.prgStatus.setText(text)
class PreviewDock(DockWidget): """GUI and implementation """ # Emitted when this window is closed. closed = pyqtSignal() def __init__(self): DockWidget.__init__(self, core.mainWindow(), "Previe&w", QIcon(':/enkiicons/internet.png'), "Alt+W") self._widget = self._createWidget() # Don't need to schedule document processing; a call to show() does. self._loadTemplates() self._widget.cbTemplate.currentIndexChanged.connect( self._onCurrentTemplateChanged) # Disconnected. # When quitting this program, don't rebuild when closing all open # documents. This can take a long time, particularly if a some of the # documents are associated with a Sphinx project. self._programRunning = True core.aboutToTerminate.connect(self._quitingApplication) # Disconnected. core.workspace().currentDocumentChanged.connect(self._onDocumentChanged) # Disconnected. core.workspace().textChanged.connect(self._onTextChanged) # Disconnected. # If the user presses the accept button in the setting dialog, Enki # will force a rebuild of the whole project. # # TODO: only build if preview settings have been changed. # # In order to make this happen, let ``_onSettingsDialogAboutToExecute`` emit # a signal indicating that the CodeChat setting dialog has been opened. Save # core.config()['Sphinx'] and core.config()['CodeChat']. After dialogAccepted # is detected, compare current settings with the old one. Build if necessary. core.uiSettingsManager().dialogAccepted.connect( self._scheduleDocumentProcessing) # Disconnected. core.workspace().modificationChanged.connect( self._onDocumentModificationChanged) # disconnected self._scrollPos = {} self._vAtEnd = {} self._hAtEnd = {} # Keep track of which Sphinx template copies we've already asked the user about. self._sphinxTemplateCheckIgnoreList = [] self._sphinxConverter = SphinxConverter(self) # stopped self._runLatest = RunLatest('QThread', parent=self) self._visiblePath = None # If we update Preview on every key press, freezes are noticable (the # GUI thread draws the preview too slowly). # This timer is used for drawing Preview 800 ms After user has stopped typing text self._typingTimer = QTimer() # stopped. self._typingTimer.setInterval(800) self._typingTimer.timeout.connect(self._scheduleDocumentProcessing) # Disconnected. self.previewSync = PreviewSync(self) # del_ called self._applyJavaScriptEnabled(self._isJavaScriptEnabled()) # Clear flags used to temporarily disable signals during # ``_scheduleDocumentProcessing.``. self._ignoreDocumentChanged = False self._ignoreTextChanges = False # Provide an inital value for the rebuild needed flag. self._rebuildNeeded = False # Save the initial font, then restore it after a ``clear``. Note that # ``clear()`` doesn't reset the `currentCharFormat # <http://doc.qt.io/qt-4.8/qplaintextedit.html#currentCharFormat>`_. In # fact, clicking in red (error/warning) message in the log window # changes the current font to red! So, save it here so that it will be # restored correctly on a ``_clear_log``. self._defaultLogFont = self._widget.teLog.currentCharFormat() # The logWindowClear signal clears the log window. self._sphinxConverter.logWindowClear.connect(self._clear_log) # disconnected # The logWindowText signal simply appends text to the log window. self._sphinxConverter.logWindowText.connect(lambda s: self._widget.teLog.appendPlainText(s)) # disconnected def _createWidget(self): widget = QWidget(self) uic.loadUi(os.path.join(os.path.dirname(__file__), 'Preview.ui'), widget) widget.layout().setContentsMargins(0, 0, 0, 0) widget.webView.page().setLinkDelegationPolicy(QWebPage.DelegateAllLinks) widget.webView.page().linkClicked.connect(self._onLinkClicked) # Disconnected. # Fix preview palette. See https://github.com/bjones1/enki/issues/34 webViewPalette = widget.webView.palette() webViewPalette.setColor(QPalette.Inactive, QPalette.HighlightedText, webViewPalette.color(QPalette.Text)) widget.webView.setPalette(webViewPalette) widget.webView.page().mainFrame().titleChanged.connect( self._updateTitle) # Disconnected. widget.cbEnableJavascript.clicked.connect( self._onJavaScriptEnabledCheckbox) # Disconnected. widget.webView.installEventFilter(self) self.setWidget(widget) self.setFocusProxy(widget.webView) widget.tbSave.clicked.connect(self.onPreviewSave) # Disconnected. # Add an attribute to ``widget`` denoting the splitter location. # This value will be overwritten when the user changes splitter location. widget.splitterErrorStateSize = (199, 50) widget.splitterNormStateSize = (1, 0) widget.splitterNormState = True widget.splitter.setSizes(widget.splitterNormStateSize) widget.splitter.splitterMoved.connect(self.on_splitterMoved) # Disconnected. return widget def _quitingApplication(self): self._programRunning = False def on_splitterMoved(self, pos, index): if self._widget.splitterNormState: self._widget.splitterNormStateSize = self._widget.splitter.sizes() else: self._widget.splitterErrorStateSize = self._widget.splitter.sizes() def terminate(self): """Uninstall themselves """ self._typingTimer.stop() self._typingTimer.timeout.disconnect(self._scheduleDocumentProcessing) try: self._widget.webView.page().mainFrame().loadFinished.disconnect( self._restoreScrollPos) except TypeError: # already has been disconnected pass self.previewSync.terminate() core.workspace().modificationChanged.disconnect( self._onDocumentModificationChanged) self._widget.cbTemplate.currentIndexChanged.disconnect( self._onCurrentTemplateChanged) core.aboutToTerminate.disconnect(self._quitingApplication) core.workspace().currentDocumentChanged.disconnect( self._onDocumentChanged) core.workspace().textChanged.disconnect(self._onTextChanged) core.uiSettingsManager().dialogAccepted.disconnect( self._scheduleDocumentProcessing) self._widget.webView.page().linkClicked.disconnect(self._onLinkClicked) self._widget.webView.page().mainFrame().titleChanged.disconnect( self._updateTitle) self._widget.cbEnableJavascript.clicked.disconnect( self._onJavaScriptEnabledCheckbox) self._widget.tbSave.clicked.disconnect(self.onPreviewSave) self._widget.splitter.splitterMoved.disconnect(self.on_splitterMoved) self._sphinxConverter.logWindowClear.disconnect(self._clear_log) self._sphinxConverter.logWindowText.disconnect() self._sphinxConverter.terminate() self._runLatest.terminate() def closeEvent(self, event): """Widget is closed. Clear it """ self.closed.emit() self._clear() return DockWidget.closeEvent(self, event) def _clear_log(self): """Clear the log window and reset the default font.""" self._widget.teLog.clear() self._widget.teLog.setCurrentCharFormat(self._defaultLogFont) def eventFilter(self, obj, ev): """ Event filter for the web view Zooms the web view """ if isinstance(ev, QWheelEvent) and \ ev.modifiers() == Qt.ControlModifier: multiplier = 1 + (0.1 * (ev.angleDelta().y() / 120.)) view = self._widget.webView view.setZoomFactor(view.zoomFactor() * multiplier) return True else: return DockWidget.eventFilter(self, obj, ev) def _onDocumentModificationChanged(self, document, modified): if not modified: # probably has been saved just now if not self._ignoreDocumentChanged: self._scheduleDocumentProcessing() def _onLinkClicked(self, url): res = QDesktopServices.openUrl(url) if res: core.mainWindow().statusBar().showMessage("{} opened in a browser".format(url.toString()), 2000) else: core.mainWindow().statusBar().showMessage("Failed to open {}".format(url.toString()), 2000) def _updateTitle(self, pageTitle): """Web page title changed. Update own title. """ if pageTitle: self.setWindowTitle("Previe&w - " + pageTitle) else: self.setWindowTitle("Previe&w") def _saveScrollPos(self): """Save scroll bar position for document """ frame = self._widget.webView .page().mainFrame() if frame.contentsSize() == QSize(0, 0): return # no valida data, nothing to save pos = frame.scrollPosition() self._scrollPos[self._visiblePath] = pos self._hAtEnd[self._visiblePath] = frame.scrollBarMaximum(Qt.Horizontal) == pos.x() self._vAtEnd[self._visiblePath] = frame.scrollBarMaximum(Qt.Vertical) == pos.y() def _restoreScrollPos(self, ok): """Restore scroll bar position for document """ try: self._widget.webView.page().mainFrame().loadFinished.disconnect(self._restoreScrollPos) except TypeError: # already has been disconnected pass if core.workspace().currentDocument() is None: return # nothing to restore if don't have document if not self._visiblePath in self._scrollPos: return # no data for this document # Don't restore the scroll position if the window is hidden. This can # happen when the current document is changed, which invokes _clear, # which calls setHtml, which calls _saveScrollPos and then this routine # when the HTML is loaded. if not self.isVisible(): return frame = self._widget.webView.page().mainFrame() frame.setScrollPosition(self._scrollPos[self._visiblePath]) if self._hAtEnd[self._visiblePath]: frame.setScrollBarValue(Qt.Horizontal, frame.scrollBarMaximum(Qt.Horizontal)) if self._vAtEnd[self._visiblePath]: frame.setScrollBarValue(Qt.Vertical, frame.scrollBarMaximum(Qt.Vertical)) # Re-sync the re-loaded text. self.previewSync.syncTextToPreview() def _onDocumentChanged(self, old, new): """Current document changed, update preview """ self._typingTimer.stop() if new is not None: if new.qutepart.language() == 'Markdown': self._widget.cbTemplate.show() self._widget.lTemplate.show() else: self._widget.cbTemplate.hide() self._widget.lTemplate.hide() self._clear() if self.isVisible(): self._scheduleDocumentProcessing() _CUSTOM_TEMPLATE_PATH = '<custom template>' def _loadTemplates(self): for path in [os.path.join(os.path.dirname(__file__), 'templates'), os.path.expanduser('~/.enki/markdown-templates')]: if os.path.isdir(path): for fileName in os.listdir(path): fullPath = os.path.join(path, fileName) if os.path.isfile(fullPath): self._widget.cbTemplate.addItem(fileName, fullPath) self._widget.cbTemplate.addItem('Custom...', self._CUSTOM_TEMPLATE_PATH) self._restorePreviousTemplate() def _restorePreviousTemplate(self): # restore previous template index = self._widget.cbTemplate.findText(core.config()['Preview']['Template']) if index != -1: self._widget.cbTemplate.setCurrentIndex(index) def _getCurrentTemplatePath(self): index = self._widget.cbTemplate.currentIndex() if index == -1: # empty combo return '' return str(self._widget.cbTemplate.itemData(index)) def _getCurrentTemplate(self): path = self._getCurrentTemplatePath() if not path: return '' try: with open(path) as file: text = file.read() except Exception as ex: text = 'Failed to load template {}: {}'.format(path, ex) core.mainWindow().statusBar().showMessage(text) return '' else: return text def _onCurrentTemplateChanged(self): """Update text or show message to the user""" if self._getCurrentTemplatePath() == self._CUSTOM_TEMPLATE_PATH: QMessageBox.information( core.mainWindow(), 'Custom templaes help', '<html>See <a href="https://github.com/hlamer/enki/wiki/Markdown-preview-templates">' 'this</a> wiki page for information about custom templates') self._restorePreviousTemplate() core.config()['Preview']['Template'] = self._widget.cbTemplate.currentText() core.config().flush() self._scheduleDocumentProcessing() def _onTextChanged(self, document): """Text changed, update preview """ if self.isVisible() and not self._ignoreTextChanges: self._typingTimer.stop() self._typingTimer.start() def show(self): """When shown, update document, if possible. """ DockWidget.show(self) self._scheduleDocumentProcessing() def _clear(self): """Clear the preview dock contents. Might be necesssary for stop executing JS and loading data. """ self._setHtml('', '', None, QUrl()) def _isJavaScriptEnabled(self): """Check if JS is enabled in the settings. """ return core.config()['Preview']['JavaScriptEnabled'] def _onJavaScriptEnabledCheckbox(self, enabled): """Checkbox clicked, save and apply settings """ core.config()['Preview']['JavaScriptEnabled'] = enabled core.config().flush() self._applyJavaScriptEnabled(enabled) def _applyJavaScriptEnabled(self, enabled): """Update QWebView settings and QCheckBox state """ self._widget.cbEnableJavascript.setChecked(enabled) settings = self._widget.webView.settings() settings.setAttribute(settings.JavascriptEnabled, enabled) def onPreviewSave(self): """Save contents of the preview pane to a user-specified file.""" path, _ = QFileDialog.getSaveFileName(self, 'Save Preview as HTML', filter='HTML (*.html)') if path: self._previewSave(path) def _previewSave(self, path): """Save contents of the preview pane to the file given by path.""" text = self._widget.webView.page().mainFrame().toHtml() try: with open(path, 'w', encoding='utf-8') as openedFile: openedFile.write(text) except (OSError, IOError) as ex: QMessageBox.critical(self, "Failed to save HTML", str(ex)) # HTML generation #---------------- # The following methods all support generation of HTML from text in the # Qutepart window in a separate thread. def _scheduleDocumentProcessing(self): """Start document processing with the thread. """ if not self._programRunning: return if self.isHidden(): return self._typingTimer.stop() document = core.workspace().currentDocument() if document is not None: if sphinxEnabledForFile(document.filePath()): self._copySphinxProjectTemplate(document.filePath()) qp = document.qutepart language = qp.language() text = qp.text sphinxCanProcess = sphinxEnabledForFile(document.filePath()) # Determine if we're in the middle of a build. currentlyBuilding = self._widget.prgStatus.text() == 'Building...' if language == 'Markdown': text = self._getCurrentTemplate() + text # Hide the progress bar, since processing is usually short and # Markdown produces no errors or warnings to display in the # progress bar. See https://github.com/bjones1/enki/issues/36. self._widget.prgStatus.setVisible(False) # Hide the error log, since Markdown never generates errors or # warnings. self._widget.teLog.setVisible(False) elif isHtmlFile(document): # No processing needed -- just display it. self._setHtml(document.filePath(), text, None, QUrl()) # Hide the progress bar, since no processing is necessary. self._widget.prgStatus.setVisible(False) # Hide the error log, since we do not HTML checking. self._widget.teLog.setVisible(False) return elif ((language == 'reStructuredText') or sphinxCanProcess or canUseCodeChat(document.filePath())): # Show the progress bar and error log for reST, CodeChat, or # Sphinx builds. It will display progress (Sphinx only) and # errors/warnings (for all three). self._widget.prgStatus.setVisible(True) self._widget.teLog.setVisible(True) self._setHtmlProgress('Building...') # Determine whether to initiate a build or not. The underlying # logic: # # - If Sphinx can't process this file, just build it. # - If Sphinx can process this file: # # - If the document isn't internally modified, we're here because # the file was saved or the refresh button was pressed. Build it. # - If the document was internally modified and "insta-build" is # enabled (i.e. build only on save is disabled): # # - If the document was not externally modified, then save and # build. # - If the document was externally modified, DANGER! The user # needs to decide which file wins (external changes or # internal changes). Don't save and build, since this would # overwrite external modifications without the user realizing # what happened. Instead, warn the user. # # As a table, see below. Build, Save, and Warn are the outputs; all # others are inputs. # # ================== =================== =================== ============= ===== ==== ==== # Sphinx can process Internally modified Externally modified Build on Save Build Save Warn # ================== =================== =================== ============= ===== ==== ==== # No X X X Yes No No # Yes No X X Yes No No # Yes Yes No No Yes Yes No # Yes Yes Yes No No No Yes # Yes Yes X Yes No No No # ================== =================== =================== ============= ===== ==== ==== internallyModified = qp.document().isModified() externallyModified = document.isExternallyModified() buildOnSave = core.config()['Sphinx']['BuildOnSave'] saveThenBuild = (sphinxCanProcess and internallyModified and not externallyModified and not buildOnSave) # If Sphinx is currently building, don't autosave -- this can # cause Sphinx to miss changes on its next build. Instead, wait # until Sphinx completes, then do a save and build. if saveThenBuild and currentlyBuilding: self._rebuildNeeded = True saveThenBuild = False else: self._rebuildNeeded = False # Save first, if needed. if saveThenBuild: # If trailing whitespace strip changes the cursor position, # restore the whitespace and cursor position. lineNum, col = qp.cursorPosition lineText = qp.lines[lineNum] # Invoking saveFile when Strip Trailing whitespace is enabled # causes ``onTextChanged`` (due to whitespace strips) and # ``onDocumentChanged`` signals to be emitted. These both # re-invoke this routine, causing a double build. So, ignore # both these signals. self._ignoreDocumentChanged = True self._ignoreTextChanges = True document.saveFile() self._ignoreDocumentChanged = False self._ignoreTextChanges = False if qp.cursorPosition != (lineNum, col): # Mark this as one operation on the undo stack. To do so, # enclose all editing operations in a context manager. See # "Text modification and Undo/Redo" in the qutepart docs. with qp: qp.lines[lineNum] = lineText qp.cursorPosition = lineNum, col qp.document().setModified(False) # Build. Each line is one row in the table above. if ((not sphinxCanProcess) or (sphinxCanProcess and not internallyModified) or saveThenBuild): # Build the HTML in a separate thread. self._runLatest.start(self._setHtmlFuture, self.getHtml, language, text, document.filePath()) # Warn. if (sphinxCanProcess and internallyModified and externallyModified and not buildOnSave): core.mainWindow().appendMessage('Warning: file modified externally. Auto-save disabled.') def getHtml(self, language, text, filePath): """Get HTML for document. This is run in a separate thread. """ if language == 'Markdown': return filePath, _convertMarkdown(text), None, QUrl() # For ReST, use docutils only if Sphinx isn't available. elif language == 'reStructuredText' and not sphinxEnabledForFile(filePath): htmlUnicode, errString = _convertReST(text) return filePath, htmlUnicode, errString, QUrl() elif filePath and sphinxEnabledForFile(filePath): # Use Sphinx to generate the HTML if possible. return self._sphinxConverter.convert(filePath) elif filePath and canUseCodeChat(filePath): # Otherwise, fall back to using CodeChat+docutils. return _convertCodeChat(text, filePath) else: return filePath, 'No preview for this type of file', None, QUrl() def _copySphinxProjectTemplate(self, documentFilePath): """Add conf.py, CodeChat.css and index.rst (if ther're missing) to the Sphinx project directory. """ if core.config()['Sphinx']['ProjectPath'] in self._sphinxTemplateCheckIgnoreList: return # Check for the existance Sphinx project files. Copy skeleton versions # of them to the project if necessary. sphinxPluginsPath = os.path.dirname(os.path.realpath(__file__)) sphinxTemplatePath = os.path.join(sphinxPluginsPath, 'sphinx_templates') sphinxProjectPath = core.config()['Sphinx']['ProjectPath'] errors = [] checklist = ['index.rst', 'conf.py'] if core.config()['CodeChat']['Enabled'] and CodeChat: checklist.append('CodeChat.css') missinglist = [] for filename in checklist: if not os.path.exists(os.path.join(sphinxProjectPath, filename)): missinglist.append(filename) if not missinglist: return errors # For testing, check for test-provided button presses if ((len(self._sphinxTemplateCheckIgnoreList) == 1) and isinstance(self._sphinxTemplateCheckIgnoreList[0], int)): res = self._sphinxTemplateCheckIgnoreList[0] else: res = QMessageBox.warning( self, r"Enki", "Sphinx project at:\n " + sphinxProjectPath + "\nis missing the template file(s): " + ' '.join(missinglist) + ". Auto-generate those file(s)?", QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel, QMessageBox.Yes) if res != QMessageBox.Yes: if res == QMessageBox.No: self._sphinxTemplateCheckIgnoreList.append(sphinxProjectPath) return if core.config()['CodeChat']['Enabled'] and CodeChat: codeChatPluginsPath = os.path.dirname(os.path.realpath(CodeChat.__file__)) codeChatTemplatePath = os.path.join(codeChatPluginsPath, 'template') copyTemplateFile(errors, codeChatTemplatePath, 'index.rst', sphinxProjectPath) copyTemplateFile(errors, codeChatTemplatePath, 'conf.py', sphinxProjectPath) copyTemplateFile(errors, codeChatTemplatePath, 'CodeChat.css', sphinxProjectPath) else: copyTemplateFile(errors, sphinxTemplatePath, 'index.rst', sphinxProjectPath) copyTemplateFile(errors, sphinxTemplatePath, 'conf.py', sphinxProjectPath) errInfo = "" for error in errors: errInfo += "Copy from " + error[0] + " to " + error[1] + " caused error " + error[2] + ';\n' if errInfo: QMessageBox.warning(self, "Sphinx template file copy error", "Copy template project files failed. The following errors are returned:<br>" + errInfo) return errors def _setHtmlFuture(self, future): """Receives a future and unpacks the result, calling _setHtml.""" filePath, htmlText, errString, baseUrl = future.result self._setHtml(filePath, htmlText, errString, baseUrl) def _setHtml(self, filePath, htmlText, errString, baseUrl): """Set HTML to the view and restore scroll bars position. Called by the thread """ self._saveScrollPos() self._visiblePath = filePath self._widget.webView.page().mainFrame().loadFinished.connect( self._restoreScrollPos) # disconnected if baseUrl.isEmpty(): # Clear the log, then update it with build content. self._widget.teLog.clear() self._widget.webView.setHtml(htmlText, baseUrl=QUrl.fromLocalFile(filePath)) else: self._widget.webView.setUrl(baseUrl) # If there were messages from the conversion process, extract a count of # errors and warnings from these messages. if errString: # If there are errors/warnings, expand log window to make it visible if self._widget.splitterNormState: self._widget.splitterNormStateSize = self._widget.splitter.sizes() self._widget.splitterNormState = False self._widget.splitter.setSizes(self._widget.splitterErrorStateSize) # This code parses the error string to determine get the number of # warnings and errors. Common docutils error messages read:: # # <string>:1589: (ERROR/3) Unknown interpreted text role "ref". # # X:\ode.py:docstring of sympy:5: (ERROR/3) Unexpected indentation. # # and common sphinx errors read:: # # X:\SVM_train.m.rst:2: SEVERE: Title overline & underline mismatch. # # X:\indexs.rst:None: WARNING: image file not readable: a.jpg # # X:\conf.py.rst:: WARNING: document isn't included in any toctree # # Each error/warning occupies one line. The following `regular # expression # <https://docs.python.org/2/library/re.html#regular-expression-syntax>`_ # is designed to find the error position (1589/None) and message # type (ERROR/WARNING/SEVERE). Extra spaces are added to show which # parts of the example string it matches. For more details about # Python regular expressions, refer to the # `re docs <https://docs.python.org/2/library/re.html>`_. # # Examining this expression one element at a time:: # # <string>:1589: (ERROR/3)Unknown interpreted text role "ref". errPosRe = ':(\d*|None): ' # Find the first occurence of a pair of colons. # Between them there can be numbers or "None" or nothing. For example, # this expression matches the string ":1589:" or string ":None:" or # string "::". Next:: # # <string>:1589: (ERROR/3)Unknown interpreted text role "ref". errTypeRe = '\(?(WARNING|ERROR|SEVERE)' # Next match the error type, which can # only be "WARNING", "ERROR" or "SEVERE". Before this error type the # message may optionally contain one left parenthesis. # errEolRe = '.*$' # Since one error message occupies one line, a ``*`` # quantifier is used along with end-of-line ``$`` to make sure only # the first match is used in each line. # # TODO: Is this necesary? Is there any case where omitting this # causes a failure? regex = re.compile(errPosRe + errTypeRe + errEolRe, # The message usually contain multiple lines; search each line # for errors and warnings. re.MULTILINE) # Use findall to return all matches in the message, not just the # first. result = regex.findall(errString) # The variable ``result`` now contains a list of tuples, where each # tuples contains the two matched groups (line number, error_string). # For example:: # # [('1589', 'ERROR')] # # Therefeore, the second element of each tuple, represented as x[1], # is the error_string. The next two lines of code will collect all # ERRORs/SEVEREs and WARNINGs found in the error_string separately. errNum = sum([x[1] == 'ERROR' or x[1] == 'SEVERE' for x in result]) warningNum = [x[1] for x in result].count('WARNING') # Report these results this to the user. status = 'Error(s): {}, warning(s): {}'.format(errNum, warningNum) # Since the error string might contain characters such as ">" and "<", # they need to be converted to ">" and "<" such that # they can be displayed correctly in the log window as HTML strings. # This step is handled by ``html.escape``. self._widget.teLog.appendHtml("<pre><font color='red'>\n" + html.escape(errString) + '</font></pre>') # Update the progress bar. color = 'red' if errNum else '#FF9955' if warningNum else None self._setHtmlProgress(status, color) else: # If there are no errors/warnings, collapse the log window (can mannually # expand it back to visible) if not self._widget.splitterNormState: self._widget.splitterErrorStateSize = self._widget.splitter.sizes() self._widget.splitterNormState = True self._widget.splitter.setSizes(self._widget.splitterNormStateSize) self._setHtmlProgress('Error(s): 0, warning(s): 0') # Do a rebuild if needed. if self._rebuildNeeded: self._rebuildNeeded = False self._scheduleDocumentProcessing() def _setHtmlProgress(self, text, color=None): """Set progress label. """ if color: style = 'QLabel { background-color: ' + color + '; }' else: style = style = 'QLabel {}' self._widget.prgStatus.setStyleSheet(style) self._widget.prgStatus.setText(text)