mirror of
https://github.com/olivierkes/manuskript.git
synced 2024-06-17 18:34:33 +12:00
754 lines
28 KiB
Python
754 lines
28 KiB
Python
#!/usr/bin/python
|
|
# -*- coding: utf-8 -*-
|
|
|
|
"""
|
|
A QSyntaxHighlighter for markdown, using tokenizer. More accurate than simple
|
|
regexp, but not yet perfect.
|
|
"""
|
|
|
|
import re
|
|
from PyQt5.QtCore import Qt, pyqtSignal, qWarning, QRegExp
|
|
from PyQt5.QtGui import (QSyntaxHighlighter, QTextBlock, QColor, QFont,
|
|
QTextCharFormat, QBrush, QPalette)
|
|
from PyQt5.QtWidgets import qApp, QStyle
|
|
|
|
from manuskript.ui.highlighters import BasicHighlighter
|
|
from manuskript.ui.highlighters import MarkdownTokenizer
|
|
from manuskript.ui.highlighters import MarkdownState as MS
|
|
from manuskript.ui.highlighters import MarkdownTokenType as MTT
|
|
from manuskript.ui.highlighters import BlockquoteStyle as BS
|
|
from manuskript.ui import style as S
|
|
from manuskript import settings
|
|
from manuskript import functions as F
|
|
|
|
# Un longue ligne. Un longue ligne. Un longue ligne. Un longue ligne.asdasdasda
|
|
|
|
GW_FADE_ALPHA = 140
|
|
|
|
# Highlighter based on GhostWriter (http://wereturtle.github.io/ghostwriter/).
|
|
# GPLV3+.
|
|
|
|
#FIXME: Setext heading don't work anymore
|
|
|
|
class MarkdownHighlighter(BasicHighlighter):
|
|
|
|
highlightBlockAtPosition = pyqtSignal(int)
|
|
headingFound = pyqtSignal(int, str, QTextBlock)
|
|
headingRemoved = pyqtSignal(int)
|
|
|
|
def __init__(self, editor):
|
|
BasicHighlighter.__init__(self, editor)
|
|
|
|
#default values
|
|
self.editor = editor
|
|
self.tokenizer = MarkdownTokenizer()
|
|
|
|
self.spellCheckEnabled = False
|
|
#self.typingPaused = True
|
|
self.inBlockquote = False
|
|
self.blockquoteStyle = BS.BlockquoteStyleFancy
|
|
|
|
# Settings
|
|
self.useUndlerlineForEmphasis = False
|
|
self.highlightLineBreaks = True
|
|
|
|
self.highlightBlockAtPosition.connect(self.onHighlightBlockAtPosition,
|
|
Qt.QueuedConnection)
|
|
|
|
self.theme = self.defaultTheme()
|
|
self.setupHeadingFontSize(True)
|
|
|
|
self.highlightedWords = []
|
|
self.highlightedTags = []
|
|
self.searchExpression = ""
|
|
self.searchExpressionRegExp = False
|
|
self.searchExpressionCase = False
|
|
|
|
#f = self.document().defaultFont()
|
|
#f.setFamily("monospace")
|
|
#self.document().setDefaultFont(f)
|
|
|
|
def transparentFormat(self, fmt, alpha=75):
|
|
"""
|
|
Takes a QTextCharFormat and modify it with colors made transparent
|
|
using alpha channel. For focus mode
|
|
"""
|
|
c = fmt.foreground().color()
|
|
c.setAlpha(alpha)
|
|
fmt.setForeground(QBrush(c))
|
|
b = fmt.background()
|
|
if b.style() != Qt.NoBrush:
|
|
c = b.color()
|
|
c.setAlpha(alpha)
|
|
fmt.setBackground(QBrush(b))
|
|
|
|
def unfocusConditions(self):
|
|
"""
|
|
Returns:
|
|
- True if the text is supposed to be unfocused
|
|
- (start, end) if block is supposed to be unfocused except for that part.
|
|
"""
|
|
|
|
if self.editor._noFocusMode or not settings.textEditor["focusMode"]:
|
|
return False
|
|
|
|
if settings.textEditor["focusMode"] == "paragraph":
|
|
return not self.currentBlock().contains(
|
|
self.editor.textCursor().position())
|
|
|
|
elif settings.textEditor["focusMode"] == "line":
|
|
if self.currentBlock().contains(
|
|
self.editor.textCursor().position()):
|
|
block = self.currentBlock()
|
|
# Position of cursor in block
|
|
pos = self.editor.textCursor().position() - block.position()
|
|
for i in range(block.layout().lineCount()):
|
|
line = block.layout().lineAt(i)
|
|
start = line.textStart()
|
|
end = line.textStart() + line.textLength()
|
|
if start <= pos <= end:
|
|
return (start, end)
|
|
else:
|
|
return True
|
|
|
|
elif settings.textEditor["focusMode"] == "sentence":
|
|
if self.currentBlock().contains(
|
|
self.editor.textCursor().position()):
|
|
block = self.currentBlock()
|
|
# Position of cursor in block
|
|
pos = self.editor.textCursor().position() - block.position()
|
|
ENDChars = "!.?"
|
|
txt = block.text()
|
|
start = -1
|
|
for i in range(len(txt)):
|
|
if start == -1: start = i
|
|
if txt[i] in ENDChars:
|
|
s = txt[start:i+1]
|
|
if start <= pos <= start + len(s):
|
|
return start, i+1
|
|
start = -1
|
|
return (start, len(txt))
|
|
|
|
else:
|
|
return True
|
|
|
|
return False
|
|
|
|
def doHighlightBlock(self, text):
|
|
"""
|
|
Note: Never set the QTextBlockFormat for a QTextBlock from within
|
|
the highlighter. Depending on how the block format is modified,
|
|
a recursive call to the highlighter may be triggered, which will
|
|
cause the application to crash.
|
|
|
|
Likewise, don't try to set the QTextBlockFormat outside the highlighter
|
|
(i.e., from within the text editor). While the application will not
|
|
crash, the format change will be added to the undo stack. Attempting
|
|
to undo from that point on will cause the undo stack to be virtually
|
|
frozen, since undoing the format operation causes the text to be
|
|
considered changed, thus triggering the slot that changes the text
|
|
formatting to be triggered yet again.
|
|
"""
|
|
|
|
lastState = self.currentBlockState()
|
|
# self.setFormat(0, len(text), self._defaultCharFormat)
|
|
|
|
# Focus mode
|
|
unfocus = self.unfocusConditions()
|
|
if unfocus:
|
|
fmt = self.format(0)
|
|
fmt.setForeground(QBrush(self.defaultTextColor))
|
|
self.transparentFormat(fmt)
|
|
if type(unfocus) != bool:
|
|
start, end = unfocus
|
|
self.setFormat(0, start, fmt)
|
|
self.setFormat(end, len(text), fmt)
|
|
else:
|
|
self.setFormat(0, len(text), fmt)
|
|
|
|
if self.tokenizer != None:
|
|
self.tokenizer.clear()
|
|
block = self.currentBlock()
|
|
nextState = MS.MarkdownStateUnknown
|
|
previousState = self.previousBlockState()
|
|
|
|
if block.next().isValid():
|
|
nextState = block.next().userState()
|
|
|
|
self.tokenizer.tokenize(text, lastState, previousState, nextState)
|
|
self.setCurrentBlockState(self.tokenizer.getState())
|
|
|
|
self.inBlockquote = self.tokenizer.getState() == MS.MarkdownStateBlockquote
|
|
|
|
# STATE FORMATTING
|
|
# FIXME: generic
|
|
if self.currentBlockState() in [
|
|
MS.MarkdownStatePipeTableHeader,
|
|
MS.MarkdownStatePipeTableDivider,
|
|
MS.MarkdownStatePipeTableRow]:
|
|
fmt = QTextCharFormat()
|
|
f = fmt.font()
|
|
f.setFamily("Monospace")
|
|
fmt.setFont(f)
|
|
self.setFormat(0, len(text), fmt)
|
|
|
|
# Monospace the blank chars
|
|
i = 0
|
|
while i <= len(text)-1 and text[i] in [" ", "\t"]:
|
|
fmt = self.format(i)
|
|
fmt.setFontFamily("Monospace")
|
|
self.setFormat(i, 1, fmt)
|
|
i += 1
|
|
|
|
#if self.currentBlockState() == MS.MarkdownStateBlockquote:
|
|
#fmt = QTextCharFormat(self._defaultCharFormat)
|
|
#fmt.setForeground(Qt.lightGray)
|
|
#self.setFormat(0, len(text), fmt)
|
|
|
|
tokens = self.tokenizer.getTokens()
|
|
|
|
for token in tokens:
|
|
if token.type == MTT.TokenUnknown:
|
|
qWarning("Highlighter found unknown token type in text block.")
|
|
continue
|
|
|
|
if token.type in [
|
|
MTT.TokenAtxHeading1,
|
|
MTT.TokenAtxHeading2,
|
|
MTT.TokenAtxHeading3,
|
|
MTT.TokenAtxHeading4,
|
|
MTT.TokenAtxHeading5,
|
|
MTT.TokenAtxHeading6,
|
|
MTT.TokenSetextHeading1Line1,
|
|
MTT.TokenSetextHeading2Line1,
|
|
]:
|
|
self.storeHeadingData(token, text)
|
|
|
|
self.applyFormattingForToken(token, text)
|
|
|
|
if self.tokenizer.backtrackRequested():
|
|
previous = self.currentBlock().previous()
|
|
self.highlightBlockAtPosition.emit(previous.position())
|
|
|
|
if self.spellCheckEnabled:
|
|
self.spellCheck(text)
|
|
|
|
# If the block has transitioned from previously being a heading to now
|
|
# being a non-heading, signal that the position in the document no
|
|
# longer contains a heading.
|
|
|
|
if self.isHeadingBlockState(lastState) and \
|
|
not self.isHeadingBlockState(self.currentBlockState()):
|
|
self.headingRemoved.emit(self.currentBlock().position())
|
|
|
|
|
|
###########################################################################
|
|
# COLORS & FORMATTING
|
|
###########################################################################
|
|
|
|
def updateColorScheme(self, rehighlight=True):
|
|
BasicHighlighter.updateColorScheme(self, rehighlight)
|
|
self.theme = self.defaultTheme()
|
|
self.setEnableLargeHeadingSizes(True)
|
|
|
|
def defaultTheme(self):
|
|
|
|
# Base Colors
|
|
background = self.backgroundColor
|
|
text = self.defaultTextColor
|
|
highlightedText = QColor(S.highlightedText)
|
|
highlightedTextDark = QColor(S.highlightedTextDark)
|
|
highlightedTextLight = QColor(S.highlightedTextLight)
|
|
highlight = QColor(S.highlight)
|
|
link = self.linkColor
|
|
linkVisited = QColor(S.linkVisited)
|
|
|
|
# titleColor = highlight
|
|
titleColor = QColor(S.highlightedTextDark)
|
|
|
|
# FullscreenEditor probably
|
|
if self.editor._fromTheme and self.editor._themeData:
|
|
text = QColor(self.editor._themeData["Text/Color"])
|
|
background = QColor(self.editor._themeData["Background/Color"])
|
|
titleColor = text
|
|
|
|
# Compositions
|
|
light = F.mixColors(text, background, .75)
|
|
markup = F.mixColors(text, background, .5)
|
|
veryLight = F.mixColors(text, background, .25)
|
|
listToken = F.mixColors(highlight, background, .4)
|
|
titleMarkupColor = F.mixColors(titleColor, background, .3)
|
|
|
|
|
|
theme = {
|
|
"markup": markup}
|
|
|
|
#Example:
|
|
#"color": Qt.red,
|
|
#"deltaSize": 10,
|
|
#"background": Qt.yellow,
|
|
#"monospace": True,
|
|
#"bold": True,
|
|
#"italic": True,
|
|
#"underline": True,
|
|
#"overline": True,
|
|
#"strike": True,
|
|
#"formatMarkup": True,
|
|
#"markupBold": True,
|
|
#"markupColor": Qt.blue,
|
|
#"markupBackground": Qt.green,
|
|
#"markupMonospace": True,
|
|
#"super":True,
|
|
#"sub":True
|
|
|
|
for i in MTT.TITLES:
|
|
theme[i] = {
|
|
"formatMarkup":True,
|
|
"bold": True,
|
|
# "monospace": True,
|
|
"markupColor": titleMarkupColor
|
|
}
|
|
|
|
theme[MTT.TokenAtxHeading1]["color"] = titleColor
|
|
theme[MTT.TokenAtxHeading2]["color"] = F.mixColors(titleColor,
|
|
background, .9)
|
|
theme[MTT.TokenAtxHeading3]["color"] = F.mixColors(titleColor,
|
|
background, .8)
|
|
theme[MTT.TokenAtxHeading4]["color"] = F.mixColors(titleColor,
|
|
background, .7)
|
|
theme[MTT.TokenAtxHeading5]["color"] = F.mixColors(titleColor,
|
|
background, .6)
|
|
theme[MTT.TokenAtxHeading6]["color"] = F.mixColors(titleColor,
|
|
background, .5)
|
|
|
|
theme[MTT.TokenSetextHeading1Line1]["color"] = titleColor
|
|
theme[MTT.TokenSetextHeading2Line1]["color"] = F.mixColors(titleColor,
|
|
background,
|
|
.9)
|
|
|
|
for i in [MTT.TokenSetextHeading1Line1, MTT.TokenSetextHeading2Line1]:
|
|
theme[i]["monospace"] = True
|
|
|
|
for i in [MTT.TokenSetextHeading1Line2, MTT.TokenSetextHeading2Line2]:
|
|
theme[i] = {
|
|
"color": titleMarkupColor,
|
|
"monospace":True}
|
|
|
|
# Beautifiers
|
|
theme[MTT.TokenEmphasis] = {
|
|
"italic":True}
|
|
theme[MTT.TokenStrong] = {
|
|
"bold":True}
|
|
theme[MTT.TokenStrikethrough] = {
|
|
"strike":True}
|
|
theme[MTT.TokenVerbatim] = {
|
|
"monospace":True,
|
|
"background": veryLight,
|
|
"formatMarkup": True,
|
|
"markupColor": markup,
|
|
"deltaSize": -1}
|
|
theme[MTT.TokenSuperScript] = {
|
|
"super":True,
|
|
"formatMarkup":True}
|
|
theme[MTT.TokenSubScript] = {
|
|
"sub":True,
|
|
"formatMarkup":True}
|
|
theme[MTT.TokenHtmlTag] = {
|
|
"color": linkVisited}
|
|
theme[MTT.TokenHtmlEntity] = { #
|
|
"color": linkVisited}
|
|
theme[MTT.TokenAutomaticLink] = {
|
|
"color": link}
|
|
theme[MTT.TokenInlineLink] = {
|
|
"color": link}
|
|
theme[MTT.TokenReferenceLink] = {
|
|
"color": link}
|
|
theme[MTT.TokenReferenceDefinition] = {
|
|
"color": link}
|
|
theme[MTT.TokenImage] = {
|
|
"color": highlightedTextDark}
|
|
theme[MTT.TokenHtmlComment] = {
|
|
"color": markup}
|
|
theme[MTT.TokenNumberedList] = {
|
|
"markupColor": listToken,
|
|
"markupBold": True,
|
|
"markupMonospace": True,}
|
|
theme[MTT.TokenBulletPointList] = {
|
|
"markupColor": listToken,
|
|
"markupBold": True,
|
|
"markupMonospace": True,}
|
|
theme[MTT.TokenHorizontalRule] = {
|
|
"overline": True,
|
|
"underline": True,
|
|
"monospace": True,
|
|
"color": markup}
|
|
theme[MTT.TokenLineBreak] = {
|
|
"background": markup}
|
|
theme[MTT.TokenBlockquote] = {
|
|
"color": light,
|
|
"markupColor": veryLight,
|
|
"markupBackground": veryLight}
|
|
theme[MTT.TokenCodeBlock] = {
|
|
"color": light,
|
|
"markupBackground": veryLight,
|
|
"formatMarkup": True,
|
|
"monospace":True,
|
|
"deltaSize":-1}
|
|
theme[MTT.TokenGithubCodeFence] = {
|
|
"color": markup}
|
|
theme[MTT.TokenPandocCodeFence] = {
|
|
"color": markup}
|
|
theme[MTT.TokenCodeFenceEnd] = {
|
|
"color": markup}
|
|
theme[MTT.TokenMention] = {} # FIXME
|
|
theme[MTT.TokenTableHeader] = {
|
|
"color": light, "monospace":True}
|
|
theme[MTT.TokenTableDivider] = {
|
|
"color": markup, "monospace":True}
|
|
theme[MTT.TokenTablePipe] = {
|
|
"color": markup, "monospace":True}
|
|
|
|
# CriticMarkup
|
|
theme[MTT.TokenCMAddition] = {
|
|
"color": QColor("#00bb00"),
|
|
"markupColor": QColor(F.mixColors("#00bb00", background, .4)),
|
|
"markupMonospace": True,}
|
|
theme[MTT.TokenCMDeletion] = {
|
|
"color": QColor("#dd0000"),
|
|
"markupColor": QColor(F.mixColors("#dd0000", background, .4)),
|
|
"markupMonospace": True,
|
|
"strike": True}
|
|
theme[MTT.TokenCMSubstitution] = {
|
|
"color": QColor("#ff8600"),
|
|
"markupColor": QColor(F.mixColors("#ff8600", background, .4)),
|
|
"markupMonospace": True,}
|
|
theme[MTT.TokenCMComment] = {
|
|
"color": QColor("#0000bb"),
|
|
"markupColor": QColor(F.mixColors("#0000bb", background, .4)),
|
|
"markupMonospace": True,}
|
|
theme[MTT.TokenCMHighlight] = {
|
|
"color": QColor("#aa53a9"),
|
|
"background": QColor(F.mixColors("#aa53a9", background, .1)),
|
|
"markupBackground": QColor(F.mixColors("#aa53a9", background, .1)),
|
|
"markupColor": QColor(F.mixColors("#aa53a9", background, .5)),
|
|
"markupMonospace": True,}
|
|
|
|
return theme
|
|
|
|
###########################################################################
|
|
# ACTUAL FORMATTING
|
|
###########################################################################
|
|
|
|
def applyFormattingForToken(self, token, text):
|
|
if token.type != MTT.TokenUnknown:
|
|
fmt = self.format(token.position + token.openingMarkupLength)
|
|
markupFormat = self.format(token.position)
|
|
if self.theme.get("markup"):
|
|
markupFormat.setForeground(self.theme["markup"])
|
|
|
|
## Debug
|
|
def debug():
|
|
print("{}\n{}{}{}{} (state:{})".format(
|
|
text,
|
|
" "*token.position,
|
|
"^"*token.openingMarkupLength,
|
|
str(token.type).center(token.length
|
|
- token.openingMarkupLength
|
|
- token.closingMarkupLength, "-"),
|
|
"^" * token.closingMarkupLength,
|
|
self.currentBlockState(),)
|
|
)
|
|
|
|
# if token.type in range(6, 10):
|
|
# debug()
|
|
|
|
theme = self.theme.get(token.type)
|
|
if theme:
|
|
fmt, markupFormat = self.formatsFromTheme(theme,
|
|
fmt,
|
|
markupFormat)
|
|
|
|
# Focus mode
|
|
unfocus = self.unfocusConditions()
|
|
if unfocus:
|
|
if (type(unfocus) == bool
|
|
or token.position < unfocus[0]
|
|
or unfocus[1] < token.position):
|
|
self.transparentFormat(fmt)
|
|
self.transparentFormat(markupFormat)
|
|
|
|
# Format opening Markup
|
|
self.setFormat(token.position, token.openingMarkupLength,
|
|
markupFormat)
|
|
|
|
# Format Text
|
|
self.setFormat(
|
|
token.position + token.openingMarkupLength,
|
|
token.length - token.openingMarkupLength - token.closingMarkupLength,
|
|
fmt)
|
|
|
|
# Format closing Markup
|
|
if token.closingMarkupLength > 0:
|
|
self.setFormat(
|
|
token.position + token.length - token.closingMarkupLength,
|
|
token.closingMarkupLength,
|
|
markupFormat)
|
|
|
|
else:
|
|
qWarning("MarkdownHighlighter.applyFormattingForToken() was passed"
|
|
" in a token of unknown type.")
|
|
|
|
def formatsFromTheme(self, theme, format=None,
|
|
markupFormat=QTextCharFormat()):
|
|
# Token
|
|
if theme.get("color"):
|
|
format.setForeground(theme["color"])
|
|
if theme.get("deltaSize"):
|
|
size = self.editor._defaultFontSize + theme["deltaSize"]
|
|
if size >= 0:
|
|
f = format.font()
|
|
f.setPointSize(size)
|
|
format.setFont(f)
|
|
if theme.get("background"):
|
|
format.setBackground(theme["background"])
|
|
if theme.get("monospace"):
|
|
format.setFontFamily("Monospace")
|
|
if theme.get("bold"):
|
|
format.setFontWeight(QFont.Bold)
|
|
if theme.get("italic"):
|
|
format.setFontItalic(theme["italic"])
|
|
if theme.get("underline"):
|
|
format.setFontUnderline(theme["underline"])
|
|
if theme.get("overline"):
|
|
format.setFontOverline(theme["overline"])
|
|
if theme.get("strike"):
|
|
format.setFontStrikeOut(theme["strike"])
|
|
if theme.get("super"):
|
|
format.setVerticalAlignment(QTextCharFormat.AlignSuperScript)
|
|
if theme.get("sub"):
|
|
format.setVerticalAlignment(QTextCharFormat.AlignSubScript)
|
|
|
|
# Markup
|
|
if theme.get("formatMarkup"):
|
|
c = markupFormat.foreground()
|
|
markupFormat = QTextCharFormat(format)
|
|
markupFormat.setForeground(c)
|
|
if theme.get("markupBold"):
|
|
markupFormat.setFontWeight(QFont.Bold)
|
|
if theme.get("markupColor"):
|
|
markupFormat.setForeground(theme["markupColor"])
|
|
if theme.get("markupBackground"):
|
|
markupFormat.setBackground(theme["markupBackground"])
|
|
if theme.get("markupMonospace"):
|
|
markupFormat.setFontFamily("Monospace")
|
|
|
|
return format, markupFormat
|
|
|
|
###########################################################################
|
|
# SETTINGS
|
|
###########################################################################
|
|
|
|
def setHighlighted(self, words, tags):
|
|
rehighlight = (self.highlightedWords != words
|
|
or self.highlightedTags != tags)
|
|
self.highlightedWords = words
|
|
self.highlightedTags = tags
|
|
if rehighlight:
|
|
self.rehighlight()
|
|
|
|
def setSearched(self, expression, regExp=False, caseSensitivity=False):
|
|
"""
|
|
Define an expression currently searched, to be highlighted.
|
|
Can be regExp.
|
|
"""
|
|
rehighlight = self.searchExpression != expression or \
|
|
self.searchExpressionRegExp != regExp or \
|
|
self.searchExpressionCase != caseSensitivity
|
|
self.searchExpression = expression
|
|
self.searchExpressionRegExp = regExp
|
|
self.searchExpressionCase = caseSensitivity
|
|
if rehighlight:
|
|
self.rehighlight()
|
|
|
|
def setDictionary(self, dictionary):
|
|
self.dictionary = dictionary
|
|
if self.spellCheckEnabled:
|
|
self.rehighlight()
|
|
|
|
def increaseFontSize(self):
|
|
self._defaultCharFormat.setFontPointSize(
|
|
self._defaultCharFormat.font().pointSize() + 1.0)
|
|
self.rehighlight()
|
|
|
|
def decreaseFontSize(self):
|
|
self._defaultCharFormat.setFontPointSize(
|
|
self._defaultCharFormat.font().pointSize() - 1.0)
|
|
self.rehighlight()
|
|
|
|
def setEnableLargeHeadingSizes(self, enable):
|
|
self.setupHeadingFontSize(enable)
|
|
self.rehighlight()
|
|
|
|
def setupHeadingFontSize(self, useLargeHeadings):
|
|
if useLargeHeadings:
|
|
self.theme[MTT.TokenSetextHeading1Line1]["deltaSize"] = 7
|
|
self.theme[MTT.TokenSetextHeading2Line1]["deltaSize"] = 5
|
|
self.theme[MTT.TokenSetextHeading1Line2]["deltaSize"] = 7
|
|
self.theme[MTT.TokenSetextHeading2Line2]["deltaSize"] = 5
|
|
self.theme[MTT.TokenAtxHeading1]["deltaSize"] = 7
|
|
self.theme[MTT.TokenAtxHeading2]["deltaSize"] = 5
|
|
self.theme[MTT.TokenAtxHeading3]["deltaSize"] = 3
|
|
self.theme[MTT.TokenAtxHeading4]["deltaSize"] = 2
|
|
self.theme[MTT.TokenAtxHeading5]["deltaSize"] = 1
|
|
self.theme[MTT.TokenAtxHeading6]["deltaSize"] = 0
|
|
|
|
else:
|
|
for i in MTT.TITLES:
|
|
self.theme[i]["deltaSize"] = 0
|
|
|
|
def setUseUnderlineForEmphasis(self, enable):
|
|
self.useUndlerlineForEmphasis = enable
|
|
self.rehighlight()
|
|
|
|
def setFont(self, fontFamily, fontSize):
|
|
font = QFont(family=fontFamily, pointSize=fontSize,
|
|
weight=QFont.Normal, italic=False)
|
|
self._defaultCharFormat.setFont(font)
|
|
self.rehighlight()
|
|
|
|
def setSpellCheckEnabled(self, enabled):
|
|
self.spellCheckEnabled = enabled
|
|
self.rehighlight()
|
|
|
|
def setBlockquoteStyle(self, style):
|
|
self.blockquoteStyle = style
|
|
|
|
if style == BS.BlockquoteStyleItalic:
|
|
self.emphasizeToken[MTT.TokenBlockquote] = True
|
|
else:
|
|
self.emphasizeToken[MTT.TokenBlockquote] = False
|
|
|
|
self.rehighlight()
|
|
|
|
def setHighlightLineBreaks(self, enable):
|
|
self.highlightLineBreaks = enable
|
|
self.rehighlight()
|
|
|
|
###########################################################################
|
|
# GHOSTWRITER SPECIFIC?
|
|
###########################################################################
|
|
|
|
def onTypingResumed(self):
|
|
self.typingPaused = False
|
|
|
|
def onTypingPaused(self):
|
|
self.typingPaused = True
|
|
block = self.document().findBlock(self.editor.textCursor().position())
|
|
self.rehighlightBlock(block)
|
|
|
|
def onHighlightBlockAtPosition(self, position):
|
|
block = self.document().findBlock(position)
|
|
self.rehighlightBlock(block)
|
|
|
|
def onTextBlockRemoved(self, block):
|
|
if self.isHeadingBlockState(block.userState):
|
|
self.headingRemoved.emit(block.position())
|
|
|
|
###########################################################################
|
|
# SPELLCHECK
|
|
###########################################################################
|
|
|
|
def spellCheck(self, text):
|
|
cursorPosition = self.editor.textCursor().position()
|
|
cursorPosBlock = self.document().findBlock(cursorPosition)
|
|
cursorPosInBlock = -1
|
|
|
|
if self.currentBlock() == cursorPosBlock:
|
|
cursorPosInBlock = cursorPosition - cursorPosBlock.position()
|
|
|
|
misspelledWord = self.dictionary.check(text, 0)
|
|
|
|
while not misspelledWord.isNull():
|
|
startIndex = misspelledWord.position()
|
|
length = misspelledWord.length()
|
|
|
|
if self.typingPaused or cursorPosInBlock != startIndex + length:
|
|
spellingErrorFormat = self.format(startIndex)
|
|
spellingErrorFormat.setUnderlineColor(self.spellingErrorColor)
|
|
spellingErrorFormat.setUnderlineStyle(
|
|
qApp.style().styleHint(QStyle.SH_SpellCheckUnderlineStyle))
|
|
|
|
self.setFormat(startIndex, length, spellingErrorFormat)
|
|
|
|
startIndex += length
|
|
misspelledWord = self.dictionary.check(text, startIndex)
|
|
|
|
def storeHeadingData(self, token, text):
|
|
if token.type in [
|
|
MTT.TokenAtxHeading1,
|
|
MTT.TokenAtxHeading2,
|
|
MTT.TokenAtxHeading3,
|
|
MTT.TokenAtxHeading4,
|
|
MTT.TokenAtxHeading5,
|
|
MTT.TokenAtxHeading6]:
|
|
level = token.type - MTT.TokenAtxHeading1 + 1
|
|
s = token.position + token.openingMarkupLength
|
|
l = (token.length
|
|
- token.openingMarkupLength
|
|
- token.closingMarkupLength)
|
|
headingText = text[s:s+l].strip()
|
|
|
|
elif token.type == MTT.TokenSetextHeading1Line1:
|
|
level = 1
|
|
headingText = text
|
|
|
|
elif token.type == MTT.TokenSetextHeading2Line1:
|
|
level = 2
|
|
headingText = text
|
|
|
|
else:
|
|
qWarning("MarkdownHighlighter.storeHeadingData() encountered" +
|
|
" unexpected token: {}".format(token.getType()))
|
|
return
|
|
|
|
# FIXME: TypeError: could not convert 'TextBlockData' to 'QTextBlockUserData'
|
|
# blockData = self.currentBlockUserData()
|
|
# if blockData == None:
|
|
# blockData = TextBlockData(self.document(), self.currentBlock())
|
|
#
|
|
# self.setCurrentBlockUserData(blockData)
|
|
self.headingFound.emit(level, headingText, self.currentBlock())
|
|
|
|
def isHeadingBlockState(self, state):
|
|
return state in [
|
|
MS.MarkdownStateAtxHeading1,
|
|
MS.MarkdownStateAtxHeading2,
|
|
MS.MarkdownStateAtxHeading3,
|
|
MS.MarkdownStateAtxHeading4,
|
|
MS.MarkdownStateAtxHeading5,
|
|
MS.MarkdownStateAtxHeading6,
|
|
MS.MarkdownStateSetextHeading1Line1,
|
|
MS.MarkdownStateSetextHeading2Line1,]
|
|
|
|
|
|
def getLuminance(color):
|
|
return (0.30 * color.redF()) + \
|
|
(0.59 * color.greenF()) + \
|
|
(0.11 * color.blueF())
|
|
|
|
|
|
def applyAlphaToChannel(foreground, background, alpha):
|
|
return (foreground * alpha) + (background * (1.0 - alpha))
|
|
|
|
|
|
def applyAlpha(foreground, background, alpha):
|
|
blendedColor = QColor(0, 0, 0)
|
|
normalizedAlpha = alpha / 255.0
|
|
blendedColor.setRed(applyAlphaToChannel(
|
|
foreground.red(), background.red(), normalizedAlpha))
|
|
blendedColor.setGreen(applyAlphaToChannel(
|
|
foreground.green(), background.green(), normalizedAlpha))
|
|
blendedColor.setBlue(applyAlphaToChannel(
|
|
foreground.blue(), background.blue(), normalizedAlpha))
|
|
return blendedColor
|