|
| 1 | +import re |
| 2 | + |
| 3 | +from qgis.PyQt.QtGui import ( |
| 4 | + QBrush, |
| 5 | + QColor, |
| 6 | + QFont, |
| 7 | + QFontMetricsF, |
| 8 | + QPalette, |
| 9 | + QSyntaxHighlighter, |
| 10 | + QTextCharFormat, |
| 11 | + QTextCursor, |
| 12 | + QTextLayout, |
| 13 | +) |
| 14 | + |
| 15 | + |
| 16 | +class MarkdownHighlighter(QSyntaxHighlighter): |
| 17 | + |
| 18 | + MARKDOWN_KEYS_REGEX = { |
| 19 | + "Bold": re.compile("(?P<delim>\*\*)(?P<text>.+)(?P=delim)"), |
| 20 | + "uBold": re.compile("(?P<delim>__)(?P<text>[^_]{2,})(?P=delim)"), |
| 21 | + "Italic": re.compile("(?P<delim>\*)(?P<text>[^*]{2,})(?P=delim)"), |
| 22 | + "uItalic": re.compile("(?P<delim>_)(?P<text>[^_]+)(?P=delim)"), |
| 23 | + "Link": re.compile("(?u)(^|(?P<pre>[^!]))\[.*?\]:?[ \t]*\(?[^)]+\)?"), |
| 24 | + "Image": re.compile("(?u)!\[.*?\]\(.+?\)"), |
| 25 | + "HeaderAtx": re.compile("(?u)^\#{1,6}(.*?)\#*(\n|$)"), |
| 26 | + "Header": re.compile("^(.+)[ \t]*\n(=+|-+)[ \t]*\n+"), |
| 27 | + "CodeBlock": re.compile("^([ ]{4,}|\t).*"), |
| 28 | + "UnorderedList": re.compile("(?u)^\s*(\* |\+ |- )+\s*"), |
| 29 | + "UnorderedListStar": re.compile("^\s*(\* )+\s*"), |
| 30 | + "OrderedList": re.compile("(?u)^\s*(\d+\. )\s*"), |
| 31 | + "BlockQuote": re.compile("(?u)^\s*>+\s*"), |
| 32 | + "BlockQuoteCount": re.compile("^[ \t]*>[ \t]?"), |
| 33 | + "CodeSpan": re.compile("(?P<delim>`+).+?(?P=delim)"), |
| 34 | + "HR": re.compile("(?u)^(\s*(\*|-)\s*){3,}$"), |
| 35 | + "eHR": re.compile("(?u)^(\s*(\*|=)\s*){3,}$"), |
| 36 | + "Html": re.compile("<.+?>"), |
| 37 | + } |
| 38 | + |
| 39 | + def __init__(self, parent): |
| 40 | + super().__init__(parent) |
| 41 | + self.parent = parent |
| 42 | + parent.setTabStopDistance( |
| 43 | + QFontMetricsF(parent.font()).horizontalAdvance(" ") * 4 |
| 44 | + ) |
| 45 | + |
| 46 | + self.defaultTheme = { |
| 47 | + "background-color": "#d7d7d7", |
| 48 | + "color": "#191970", |
| 49 | + "bold": {"color": "#859900", "font-weight": "bold", "font-style": "normal"}, |
| 50 | + "emphasis": { |
| 51 | + "color": "#b58900", |
| 52 | + "font-weight": "bold", |
| 53 | + "font-style": "italic", |
| 54 | + }, |
| 55 | + "link": { |
| 56 | + "color": "#cb4b16", |
| 57 | + "font-weight": "normal", |
| 58 | + "font-style": "normal", |
| 59 | + }, |
| 60 | + "image": { |
| 61 | + "color": "#cb4b16", |
| 62 | + "font-weight": "normal", |
| 63 | + "font-style": "normal", |
| 64 | + }, |
| 65 | + "header": { |
| 66 | + "color": "#2aa198", |
| 67 | + "font-weight": "bold", |
| 68 | + "font-style": "normal", |
| 69 | + }, |
| 70 | + "unorderedlist": { |
| 71 | + "color": "#dc322f", |
| 72 | + "font-weight": "normal", |
| 73 | + "font-style": "normal", |
| 74 | + }, |
| 75 | + "orderedlist": { |
| 76 | + "color": "#dc322f", |
| 77 | + "font-weight": "normal", |
| 78 | + "font-style": "normal", |
| 79 | + }, |
| 80 | + "blockquote": { |
| 81 | + "color": "#dc322f", |
| 82 | + "font-weight": "normal", |
| 83 | + "font-style": "normal", |
| 84 | + }, |
| 85 | + "codespan": { |
| 86 | + "color": "#dc322f", |
| 87 | + "font-weight": "normal", |
| 88 | + "font-style": "normal", |
| 89 | + }, |
| 90 | + "codeblock": { |
| 91 | + "color": "#ff9900", |
| 92 | + "font-weight": "normal", |
| 93 | + "font-style": "normal", |
| 94 | + }, |
| 95 | + "line": { |
| 96 | + "color": "#2aa198", |
| 97 | + "font-weight": "normal", |
| 98 | + "font-style": "normal", |
| 99 | + }, |
| 100 | + "html": { |
| 101 | + "color": "#c000c0", |
| 102 | + "font-weight": "normal", |
| 103 | + "font-style": "normal", |
| 104 | + }, |
| 105 | + } |
| 106 | + self.setTheme(self.defaultTheme) |
| 107 | + |
| 108 | + def setTheme(self, theme): |
| 109 | + self.theme = theme |
| 110 | + self.MARKDOWN_KWS_FORMAT = {} |
| 111 | + |
| 112 | + pal = self.parent.palette() |
| 113 | + pal.setColor(QPalette.Base, QColor(theme["background-color"])) |
| 114 | + self.parent.setPalette(pal) |
| 115 | + self.parent.setTextColor(QColor(theme["color"])) |
| 116 | + |
| 117 | + text_char_format = QTextCharFormat() |
| 118 | + text_char_format.setForeground(QBrush(QColor(theme["header"]["color"]))) |
| 119 | + text_char_format.setFontWeight( |
| 120 | + QFont.Bold if theme["header"]["font-weight"] == "bold" else QFont.Normal |
| 121 | + ) |
| 122 | + text_char_format.setFontItalic( |
| 123 | + True if theme["header"]["font-style"] == "italic" else False |
| 124 | + ) |
| 125 | + self.MARKDOWN_KWS_FORMAT["HeaderAtx"] = text_char_format |
| 126 | + |
| 127 | + self.rehighlight() |
| 128 | + |
| 129 | + def highlightBlock(self, text): |
| 130 | + text = str(text) |
| 131 | + self.highlightMarkdown(text, 0) |
| 132 | + |
| 133 | + def highlightMarkdown(self, text, strt): |
| 134 | + cursor = QTextCursor(self.document()) |
| 135 | + bf = cursor.blockFormat() |
| 136 | + self.setFormat(0, len(text), QColor(self.theme["color"])) |
| 137 | + |
| 138 | + if self.highlightAtxHeader(text, cursor, bf, strt): |
| 139 | + return |
| 140 | + |
| 141 | + def highlightEmptyLine(self, text, cursor, bf, strt): |
| 142 | + textAscii = str(text.replace("\u2029", "\n")) |
| 143 | + if textAscii.strip(): |
| 144 | + return False |
| 145 | + else: |
| 146 | + return True |
| 147 | + |
| 148 | + def highlightAtxHeader(self, text, cursor, bf, strt): |
| 149 | + found = False |
| 150 | + for mo in re.finditer(self.MARKDOWN_KEYS_REGEX["HeaderAtx"], text): |
| 151 | + # bf.setBackground(QBrush(QColor(7,54,65))) |
| 152 | + # cursor.movePosition(QTextCursor.End) |
| 153 | + # cursor.mergeBlockFormat(bf) |
| 154 | + self.setFormat( |
| 155 | + mo.start() + strt, |
| 156 | + mo.end() - mo.start(), |
| 157 | + self.MARKDOWN_KWS_FORMAT["HeaderAtx"], |
| 158 | + ) |
| 159 | + found = True |
| 160 | + return found |
0 commit comments