# -*- coding: utf-8 -*- """novelWriter GUI Document Highlighter novelWriter – GUI Document Highlighter ======================================== Syntax highlighting for MarkDown File History: Created: 2019-04-06 [0.0.1] """ import logging import nw from PyQt5.QtCore import Qt, QRegularExpression from PyQt5.QtGui import ( QColor, QTextCharFormat, QFont, QSyntaxHighlighter, QBrush ) from nw.constants import nwUnicode logger = logging.getLogger(__name__) class GuiDocHighlighter(QSyntaxHighlighter): def __init__(self, theDoc, theParent): QSyntaxHighlighter.__init__(self, theDoc) logger.debug("Initialising DocHighlighter ...") self.mainConf = nw.CONFIG self.theDoc = theDoc self.theParent = theParent self.theTheme = theParent.theTheme self.theIndex = theParent.theIndex self.theDict = None self.theHandle = None self.spellCheck = False self.spellRx = None self.hRules = [] self.hStyles = {} self.colHead = QColor(0,0,0) self.colHeadH = QColor(0,0,0) self.colEmph = QColor(0,0,0) self.colDialN = QColor(0,0,0) self.colDialD = QColor(0,0,0) self.colDialS = QColor(0,0,0) self.colComm = QColor(0,0,0) self.colKey = QColor(0,0,0) self.colVal = QColor(0,0,0) self.colSpell = QColor(0,0,0) self.colTagErr = QColor(0,0,0) self.colRepTag = QColor(0,0,0) self.initHighlighter() logger.debug("DocHighlighter initialisation complete") return def initHighlighter(self): logger.debug("Setting up highlighting rules") self.colHead = QColor(*self.theTheme.colHead) self.colHeadH = QColor(*self.theTheme.colHeadH) self.colEmph = QColor(*self.theTheme.colEmph) self.colDialN = QColor(*self.theTheme.colDialN) self.colDialD = QColor(*self.theTheme.colDialD) self.colDialS = QColor(*self.theTheme.colDialS) self.colComm = QColor(*self.theTheme.colComm) self.colKey = QColor(*self.theTheme.colKey) self.colVal = QColor(*self.theTheme.colVal) self.colSpell = QColor(*self.theTheme.colSpell) self.colTagErr = QColor(*self.theTheme.colTagErr) self.colRepTag = QColor(*self.theTheme.colRepTag) self.colMod = QColor(*self.theTheme.colMod) self.colTrail = QColor(*self.theTheme.colEmph,64) self.hStyles = { "header1" : self._makeFormat(self.colHead, "bold",1.8), "header2" : self._makeFormat(self.colHead, "bold",1.6), "header3" : self._makeFormat(self.colHead, "bold",1.4), "header4" : self._makeFormat(self.colHead, "bold",1.2), "header1h" : self._makeFormat(self.colHeadH,"bold",1.8), "header2h" : self._makeFormat(self.colHeadH,"bold",1.6), "header3h" : self._makeFormat(self.colHeadH,"bold",1.4), "header4h" : self._makeFormat(self.colHeadH,"bold",1.2), "bold" : self._makeFormat(self.colEmph, "bold"), "italic" : self._makeFormat(self.colEmph, "italic"), "strike" : self._makeFormat(self.colEmph, "strike"), "underline" : self._makeFormat(self.colEmph, "underline"), "trailing" : self._makeFormat(self.colTrail,"background"), "nobreak" : self._makeFormat(self.colTrail,"background"), "dialogue1" : self._makeFormat(self.colDialN), "dialogue2" : self._makeFormat(self.colDialD), "dialogue3" : self._makeFormat(self.colDialS), "replace" : self._makeFormat(self.colRepTag), "hidden" : self._makeFormat(self.colComm), "keyword" : self._makeFormat(self.colKey), "modifier" : self._makeFormat(self.colMod), "value" : self._makeFormat(self.colVal), } self.hRules = [] # Trailing Spaces, 2+ self.hRules.append(( r"[ ]{2,}$", { 0 : self.hStyles["trailing"], } )) # Non-breaking Space self.hRules.append(( "[%s]+" % nwUnicode.U_NBSP, { 0 : self.hStyles["nobreak"], } )) # Markdown self.hRules.append(( r"(?", { 0 : self.hStyles["replace"], } )) # Build a QRegExp for each highlight pattern self.rxRules = [] for regEx, regRules in self.hRules: hReg = QRegularExpression(regEx) hReg.setPatternOptions(QRegularExpression.UseUnicodePropertiesOption) self.rxRules.append((hReg, regRules)) # Build a QRegExp for spell checker # Include additional characters that the highlighter should # consider to be word separators wordSep = "_+" wordSep += nwUnicode.U_EMDASH self.spellRx = QRegularExpression("\\b[^\\s%s]+\\b" % wordSep) self.spellRx.setPatternOptions(QRegularExpression.UseUnicodePropertiesOption) return True ## # Setters ## def setDict(self, theDict): self.theDict = theDict return True def setSpellCheck(self, theMode): self.spellCheck = theMode return True def setHandle(self, theHandle): self.theHandle = theHandle return True ## # Highlight Block ## def highlightBlock(self, theText): """Highlight a single block. Prefer to check first character for all formats that are defined by their initial characters. This is significantly faster than running the regex checks we use for text paragraphs. """ if self.theHandle is None or not theText: return if theText.startswith("@"): # Keywords and commands tItem = self.theParent.theProject.getItem(self.theHandle) isValid, theBits, thePos = self.theIndex.scanThis(theText) isGood = self.theIndex.checkThese(theBits, tItem) if isValid: for n in range(len(theBits)): xPos = thePos[n] xLen = len(theBits[n]) if isGood[n]: if n == 0: self.setFormat(xPos, xLen, self.hStyles["keyword"]) else: self.setFormat(xPos, xLen, self.hStyles["value"]) else: kwFmt = self.format(xPos) kwFmt.setUnderlineColor(self.colTagErr) kwFmt.setUnderlineStyle(QTextCharFormat.SpellCheckUnderline) self.setFormat(xPos, xLen, kwFmt) # We never want to run the spell checker on keyword/values, # so we force a return here return elif theText.startswith("# "): # Header 1 self.setFormat(0, 1, self.hStyles["header1h"]) self.setFormat(1, len(theText), self.hStyles["header1"]) elif theText.startswith("## "): # Header 2 self.setFormat(0, 2, self.hStyles["header2h"]) self.setFormat(2, len(theText), self.hStyles["header2"]) elif theText.startswith("### "): # Header 3 self.setFormat(0, 3, self.hStyles["header3h"]) self.setFormat(3, len(theText), self.hStyles["header3"]) elif theText.startswith("#### "): # Header 4 self.setFormat(0, 4, self.hStyles["header4h"]) self.setFormat(4, len(theText), self.hStyles["header4"]) elif theText.startswith("%"): # Comments if theText.startswith("%synopsis:"): self.setFormat(0, 10, self.hStyles["modifier"]) self.setFormat(10, len(theText), self.hStyles["hidden"]) else: self.setFormat(0, len(theText), self.hStyles["hidden"]) else: # Text Paragraph for rX, xFmt in self.rxRules: rxItt = rX.globalMatch(theText, 0) while rxItt.hasNext(): rxMatch = rxItt.next() for xM in xFmt.keys(): xPos = rxMatch.capturedStart(xM) xLen = rxMatch.capturedLength(xM) self.setFormat(xPos, xLen, xFmt[xM]) if self.theDict is None or not self.spellCheck: return rxSpell = self.spellRx.globalMatch(theText, 0) while rxSpell.hasNext(): rxMatch = rxSpell.next() if not self.theDict.checkWord(rxMatch.captured(0)): if rxMatch.captured(0) == rxMatch.captured(0).upper(): continue xPos = rxMatch.capturedStart(0) xLen = rxMatch.capturedLength(0) for x in range(xLen): spFmt = self.format(xPos+x) spFmt.setUnderlineColor(self.colSpell) spFmt.setUnderlineStyle(QTextCharFormat.SpellCheckUnderline) self.setFormat(xPos+x, 1, spFmt) return ## # Internal Functions ## def _makeFormat(self, fmtCol=None, fmtStyle=None, fmtSize=None): """Generate a valid character format to be applied to the text that is to be highlighted. """ theFormat = QTextCharFormat() if fmtCol is not None: theFormat.setForeground(fmtCol) if fmtStyle is not None: if "bold" in fmtStyle: theFormat.setFontWeight(QFont.Bold) if "italic" in fmtStyle: theFormat.setFontItalic(True) if "strike" in fmtStyle: theFormat.setFontStrikeOut(True) if "underline" in fmtStyle: theFormat.setFontUnderline(True) if "background" in fmtStyle: theFormat.setBackground(QBrush(fmtCol,Qt.SolidPattern)) if fmtSize is not None: theFormat.setFontPointSize(round(fmtSize*self.mainConf.textSize)) return theFormat # END Class DocHighlighter