diff --git a/src/calibre/gui2/comments_editor.py b/src/calibre/gui2/comments_editor.py index 32e93d0a77..547dd51848 100644 --- a/src/calibre/gui2/comments_editor.py +++ b/src/calibre/gui2/comments_editor.py @@ -9,15 +9,16 @@ __docformat__ = 'restructuredtext en' from lxml import html from lxml.html import soupparser -from PyQt4.Qt import QApplication, QFontInfo, QSize, QWidget, \ - QToolBar, QVBoxLayout, QAction, QIcon, QWebPage, Qt +from PyQt4.Qt import QApplication, QFontInfo, QSize, QWidget, QPlainTextEdit, \ + QToolBar, QVBoxLayout, QAction, QIcon, QWebPage, Qt, QTabWidget, \ + QSyntaxHighlighter, QColor, QChar from PyQt4.QtWebKit import QWebView from calibre.ebooks.chardet import xml_to_unicode from calibre import xml_replace_entities -class PageAction(QAction): +class PageAction(QAction): # {{{ def __init__(self, wac, icon, text, checkable, view): QAction.__init__(self, QIcon(I(icon+'.png')), text, view) @@ -41,8 +42,9 @@ class PageAction(QAction): self.setChecked(self.page_action.isChecked()) self.setEnabled(self.page_action.isEnabled()) +# }}} -class EditorWidget(QWebView): +class EditorWidget(QWebView): # {{{ def __init__(self, parent=None): QWebView.__init__(self, parent) @@ -132,6 +134,219 @@ class EditorWidget(QWebView): return property(fget=fget, fset=fset) +# }}} + +# Highlighter {{{ +State_Text = -1 +State_DocType = 0 +State_Comment = 1 +State_TagStart = 2 +State_TagName = 3 +State_InsideTag = 4 +State_AttributeName = 5 +State_SingleQuote = 6 +State_DoubleQuote = 7 +State_AttributeValue = 8 + +class Highlighter(QSyntaxHighlighter): + + def __init__(self, doc): + QSyntaxHighlighter.__init__(self, doc) + self.colors = {} + self.colors['doctype'] = QColor(192, 192, 192) + self.colors['entity'] = QColor(128, 128, 128) + self.colors['tag'] = QColor(136, 18, 128) + self.colors['comment'] = QColor( 35, 110, 37) + self.colors['attrname'] = QColor(153, 69, 0) + self.colors['attrval'] = QColor( 36, 36, 170) + + def highlightBlock(self, text): + state = self.previousBlockState() + len_ = text.length() + start = 0 + pos = 0 + + while pos < len_: + + if state == State_Comment: + start = pos + while pos < len_: + if text.mid(pos, 3) == "-->": + pos += 3; + state = State_Text; + break + else: + pos += 1 + self.setFormat(start, pos - start, self.colors['comment']) + + elif state == State_DocType: + start = pos + while pos < len_: + ch = text.at(pos) + pos += 1 + if ch == QChar('>'): + state = State_Text + break + self.setFormat(start, pos - start, self.colors['doctype']) + + # at '<' in e.g. "foo" + elif state == State_TagStart: + start = pos + 1 + while pos < len_: + ch = text.at(pos) + pos += 1 + if ch == QChar('>'): + state = State_Text + break + if not ch.isSpace(): + pos -= 1 + state = State_TagName + break + + # at 'b' in e.g "
foo
" + elif state == State_TagName: + start = pos + while pos < len_: + ch = text.at(pos) + pos += 1 + if ch.isSpace(): + pos -= 1 + state = State_InsideTag + break + if ch == QChar('>'): + state = State_Text + break + self.setFormat(start, pos - start, self.colors['tag']); + + # anywhere after tag name and before tag closing ('>') + elif state == State_InsideTag: + start = pos + + while pos < len_: + ch = text.at(pos) + pos += 1 + + if ch == QChar('/'): + continue + + if ch == QChar('>'): + state = State_Text + break + + if not ch.isSpace(): + pos -= 1 + state = State_AttributeName + break + + # at 's' in e.g. + elif state == State_AttributeName: + start = pos + + while pos < len_: + ch = text.at(pos) + pos += 1 + + if ch == QChar('='): + state = State_AttributeValue + break + + if ch in (QChar('>'), QChar('/')): + state = State_InsideTag + break + + self.setFormat(start, pos - start, self.colors['attrname']) + + # after '=' in e.g. + elif state == State_AttributeValue: + start = pos + + # find first non-space character + while pos < len_: + ch = text.at(pos) + pos += 1 + + # handle opening single quote + if ch == QChar("'"): + state = State_SingleQuote + break + + # handle opening double quote + if ch == QChar('"'): + state = State_DoubleQuote + break + + if not ch.isSpace(): + break + + if state == State_AttributeValue: + # attribute value without quote + # just stop at non-space or tag delimiter + start = pos + while pos < len_: + ch = text.at(pos); + if ch.isSpace(): + break + if ch in (QChar('>'), QChar('/')): + break + pos += 1 + state = State_InsideTag + self.setFormat(start, pos - start, self.colors['attrval']) + + # after the opening single quote in an attribute value + elif state == State_SingleQuote: + start = pos + + while pos < len_: + ch = text.at(pos) + pos += 1 + if ch == QChar("'"): + break + + state = State_InsideTag + + self.setFormat(start, pos - start, self.colors['attrval']) + + # after the opening double quote in an attribute value + elif state == State_DoubleQuote: + start = pos + + while pos < len_: + ch = text.at(pos) + pos += 1 + if ch == QChar('"'): + break + + state = State_InsideTag + + self.setFormat(start, pos - start, self.colors['attrval']) + + else: + # State_Text and default + while pos < len_: + ch = text.at(pos) + if ch == QChar('<'): + if text.mid(pos, 4) == "