Files
novelWriter/nw/gui/tools/dochighlight.py
T
2019-11-20 19:09:32 +01:00

322 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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"(?<![\w|\\])([\*]{2})(?!\s)(?m:(.+?))(?<![\s|\\])(\1)(?!\w)", {
1 : self.hStyles["hidden"],
2 : self.hStyles["bold"],
3 : self.hStyles["hidden"],
}
))
self.hRules.append((
r"(?<![\w|_|\\])([_])(?!\s|\1)(?m:(.+?))(?<![\s|\\])(\1)(?!\w)", {
1 : self.hStyles["hidden"],
2 : self.hStyles["italic"],
3 : self.hStyles["hidden"],
}
))
self.hRules.append((
r"(?<![\w|\\])([_]{2})(?!\s)(?m:(.+?))(?<![\s|\\])(\1)(?!\w)", {
1 : self.hStyles["hidden"],
2 : self.hStyles["underline"],
3 : self.hStyles["hidden"],
}
))
# Quoted Strings
self.hRules.append((
"{:s}(.+?){:s}".format('"','"'), {
0 : self.hStyles["dialogue1"],
}
))
self.hRules.append((
"{:s}(.+?){:s}".format(*self.mainConf.fmtDoubleQuotes), {
0 : self.hStyles["dialogue2"],
}
))
self.hRules.append((
"{:s}(.+?){:s}".format(*self.mainConf.fmtSingleQuotes), {
0 : self.hStyles["dialogue3"],
}
))
self.hRules.append((
r"<(\S+?)>", {
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