Welcome to mirror list, hosted at ThFree Co, Russian Federation.

dev.gajim.org/gajim/gajim-plugins.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Münchbach <182-FlorianMuenchbach@users.noreply.dev.gajim.org>2019-02-17 23:33:44 +0300
committerFlorian Münchbach <182-FlorianMuenchbach@users.noreply.dev.gajim.org>2019-02-17 23:43:55 +0300
commit1b12a0388b1b9e8f9b336d988569f05a0b76a9b9 (patch)
tree6d281bc302f96a19c7470da852ab69b244e4f5e5
parenta1e0c41550fa1883440bd379fe5313364200896d (diff)
[syntax_highlight] Split code in individual files for maintainability
-rw-r--r--syntax_highlight/chat_syntax_highlighter.py266
-rw-r--r--syntax_highlight/plugin_config.py141
-rw-r--r--syntax_highlight/plugin_config_dialog.py170
-rw-r--r--syntax_highlight/syntax_highlight.py577
-rw-r--r--syntax_highlight/types.py19
5 files changed, 602 insertions, 571 deletions
diff --git a/syntax_highlight/chat_syntax_highlighter.py b/syntax_highlight/chat_syntax_highlighter.py
new file mode 100644
index 0000000..9fd888e
--- /dev/null
+++ b/syntax_highlight/chat_syntax_highlighter.py
@@ -0,0 +1,266 @@
+import logging
+import re
+import pygments
+
+from gi.repository import Gtk
+
+from gajim.plugins.helpers import log_calls, log
+
+
+from .gtkformatter import GTKFormatter
+from .types import MatchType, LineBreakOptions, CodeMarkerOptions
+
+
+log = logging.getLogger('gajim.plugin_system.syntax_highlight')
+
+class ChatSyntaxHighlighter:
+ def hide_code_markup(self, buf, start, end):
+ tag = buf.get_tag_table().lookup('hide_code_markup')
+ if tag is None:
+ tag = Gtk.TextTag.new('hide_code_markup')
+ tag.set_property('invisible', True)
+ buf.get_tag_table().add(tag)
+
+ buf.apply_tag_by_name('hide_code_markup', start, end)
+
+ def check_line_break(self, is_multiline):
+ line_break = self.config.get_line_break_action()
+
+ return (line_break == LineBreakOptions.ALWAYS) \
+ or (is_multiline and line_break == LineBreakOptions.MULTILINE)
+
+
+ def format_code(self, buf, s_tag, s_code, e_tag, e_code, language):
+ style = self.config.get_style_name()
+ if self.config.get_code_marker_setting() == CodeMarkerOptions.HIDE:
+ self.hide_code_markup(buf, s_tag, s_code)
+ self.hide_code_markup(buf, e_code, e_tag)
+ else:
+ comment_tag = GTKFormatter.create_tag_for_token(
+ pygments.token.Comment,
+ pygments.styles.get_style_by_name(style))
+ buf.get_tag_table().add(comment_tag)
+ buf.apply_tag(comment_tag, s_tag, s_code)
+ buf.apply_tag(comment_tag, e_tag, e_code)
+
+ code = s_code.get_text(e_code)
+ log.debug("full text to encode: %s.", code)
+
+
+ start_mark = buf.create_mark(None, s_code, False)
+
+ lexer = None
+
+ if language is None:
+ lexer = self.config.get_default_lexer()
+ log.info("No Language specified. Falling back to default lexer: %s.",
+ self.config.get_default_lexer_name())
+ else:
+ log.debug("Using lexer for %s.", str(language))
+ lexer = self.config.get_lexer_with_fallback(language)
+
+ if lexer is None:
+ iterator = buf.get_iter_at_mark(start_mark)
+ buf.insert(iterator, '\n')
+ else:
+ tokens = pygments.lex(code, lexer)
+
+ formatter = GTKFormatter(style=style, start_mark=start_mark)
+ pygments.format(tokens, formatter, buf)
+
+ def find_multiline_matches(self, text):
+ start = None
+ matches = []
+ for i in re.finditer(r'\n?```(?:\S*\n)?', text, re.DOTALL):
+ if start is None:
+ start = i
+ elif re.match(r'^\n```', i.group(0)) is not None:
+ matches.append(
+ (start.start(), i.end(), text[start.start():i.end()]))
+ start = None
+ else:
+ # not an end...
+ continue
+ return matches
+
+ def find_inline_matches(self, text):
+ return [(i.start(), i.end(), i.group(0)) for i in \
+ re.finditer(r'(?<!`)`(?!`|\n).+(?<!`)`', text)]
+
+ def merge_match_groups(self, real_text, inline_matches, multiline_matches):
+ it_inline = iter(inline_matches)
+ it_multi = iter(multiline_matches)
+ length = len(real_text)
+
+ # Just to get cleaner code below...
+ def get_next(iterator):
+ return next(iterator, (length, length, ""))
+
+ # In order to simplify the process, we use the 'length' here.
+ cur_inline = get_next(it_inline)
+ cur_multi = get_next(it_multi)
+
+ pos = 0
+
+ # This will contain tuples with parts of the input and its classification
+ parts = []
+ while pos < length:
+ log.debug("-> in: %s", str(cur_inline))
+ log.debug("-> mu: %s", str(cur_multi))
+
+ # selected = (start, end, type)
+ selected = (cur_inline[0], cur_inline[1], MatchType.INLINE) \
+ if cur_inline[0] < cur_multi[0] \
+ else (cur_multi[0], cur_multi[1], MatchType.MULTILINE) \
+ if cur_multi[0] < length \
+ else (pos, length, MatchType.TEXT)
+ log.debug("--> select: %s", str(selected))
+
+ # Handle plain text string parts (and unforseen errors...)
+ if pos < selected[0]:
+ end = selected[0] if selected[0] != pos else selected[1]
+ parts.append((real_text[pos:end], MatchType.TEXT))
+ pos = selected[0]
+ elif pos > selected[0]:
+ log.error("Should not happen, position > found match.")
+
+ # Cut out and append selected text segment
+ parts.append((real_text[selected[0]:selected[1]], selected[2]))
+ pos = selected[1]
+
+ # Depending on the match type, we have to forward the iterators.
+ # Also, forward the other one, if regions overlap or we took over...
+ if selected[2] == MatchType.INLINE:
+ if cur_multi[0] < cur_inline[1]:
+ cur_multi = get_next(it_multi)
+ cur_inline = get_next(it_inline)
+ elif selected[2] == MatchType.MULTILINE:
+ if cur_inline[0] < cur_multi[1]:
+ cur_inline = get_next(it_inline)
+ cur_multi = get_next(it_multi)
+
+ return parts
+
+ def process_text(self, real_text, other_tags, _graphics, iter_,
+ _additional):
+ def fix_newline(char, marker_len_no_newline, force=False):
+ fixed = (marker_len_no_newline, '')
+ if char == '\n':
+ fixed = (marker_len_no_newline + 1, '')
+ elif force:
+ fixed = (marker_len_no_newline + 1, '\n')
+ return fixed
+
+
+ buf = self.textview.tv.get_buffer()
+
+ # first, try to find inline or multiline code snippets
+ inline_matches = self.find_inline_matches(real_text)
+ multiline_matches = self.find_multiline_matches(real_text)
+
+ if not inline_matches and not multiline_matches:
+ log.debug("Stopping early, since there is no code block in it....")
+ return
+
+ iterator = iter_ if iter_ is not None else buf.get_end_iter()
+
+ # Create a start marker with left gravity before inserting text.
+ start_mark = buf.create_mark("SHP_start", iterator, True)
+ end_mark = buf.create_mark("SHP_end", iterator, False)
+
+ insert_newline_for_multiline = self.check_line_break(True)
+ insert_newline_for_inline = self.check_line_break(False)
+
+ split_text = self.merge_match_groups(
+ real_text, inline_matches, multiline_matches)
+
+ buf.begin_user_action()
+
+ for num, (text_to_insert, match_type) in enumerate(split_text):
+ marker = [("", 0), ("", 0)]
+ language = None
+ end_of_message = num == (len(split_text) - 1)
+
+ if match_type == MatchType.TEXT:
+ self.textview.detect_and_print_special_text(
+ text_to_insert, other_tags, graphics=_graphics,
+ iter_=iterator, additional_data=_additional)
+ else:
+ if match_type == MatchType.MULTILINE:
+ language_match = re.search(
+ '\n*```([^\n]*)\n', text_to_insert, re.DOTALL)
+ language = None if language_match is None \
+ else language_match.group(1)
+ language_len = 0 if language is None else len(language)
+
+ # We account the language word width for the front marker
+ front = fix_newline(text_to_insert[0], 3 + language_len,
+ insert_newline_for_multiline)
+ back = fix_newline(text_to_insert[-1], 3,
+ insert_newline_for_multiline and not end_of_message)
+ else:
+ front = fix_newline(text_to_insert[0], 1,
+ insert_newline_for_inline)
+ back = fix_newline(text_to_insert[-1], 1,
+ insert_newline_for_inline and not end_of_message)
+
+ marker_widths = (front[0], back[0])
+ text_to_insert = ''.join([front[1], text_to_insert, back[1]])
+
+ # insertion invalidates iterator, let's use our start mark...
+ self.insert_and_format_code(buf, text_to_insert, language,
+ marker_widths, start_mark, other_tags)
+
+ iterator = buf.get_iter_at_mark(end_mark)
+ # the current end of the buffer's contents is the start for the
+ # next iteration
+ buf.move_mark(start_mark, iterator)
+
+ buf.delete_mark(start_mark)
+ buf.delete_mark(end_mark)
+
+ buf.end_user_action()
+
+ # We have to make sure this is the last thing we do (i.e. no calls to
+ # the other textview methods no more from here on), because the
+ # print_special_text method is resetting the plugin_modified variable...
+ self.textview.plugin_modified = True
+
+ def insert_and_format_code(self, buf, insert_text, language, marker, start_mark, other_tags=None):
+ start_iter = buf.get_iter_at_mark(start_mark)
+
+ if other_tags:
+ buf.insert_with_tags_by_name(start_iter, insert_text,
+ *other_tags)
+ else:
+ buf.insert(start_iter, insert_text)
+
+ start_iter = buf.get_iter_at_mark(start_mark)
+ tag_start = start_iter
+ tag_end = buf.get_end_iter()
+ s_code = start_iter.copy()
+ e_code = tag_end.copy()
+ s_code.forward_chars(marker[0])
+ e_code.backward_chars(marker[1])
+
+ log.debug("full text between tags: %s.", tag_start.get_text(tag_end))
+
+ self.format_code(buf, tag_start, s_code, tag_end, e_code, language)
+
+ self.textview.plugin_modified = True
+
+ # Set general code block format
+ tag = Gtk.TextTag.new()
+ if self.config.is_bgcolor_override_enabled():
+ tag.set_property('background', self.config.get_bgcolor())
+ tag.set_property('paragraph-background', self.config.get_bgcolor())
+ tag.set_property('font', self.config.get_font())
+ buf.get_tag_table().add(tag)
+ buf.apply_tag(tag, start_iter, buf.get_end_iter())
+
+ def __init__(self, config, textview):
+ self.last_end_mark = None
+ self.config = config
+ self.textview = textview
+
+
diff --git a/syntax_highlight/plugin_config.py b/syntax_highlight/plugin_config.py
new file mode 100644
index 0000000..97a36a0
--- /dev/null
+++ b/syntax_highlight/plugin_config.py
@@ -0,0 +1,141 @@
+from gajim.plugins.helpers import log_calls, log
+
+from pygments.lexers import get_lexer_by_name, get_all_lexers
+from pygments.styles import get_all_styles
+
+from .types import MatchType, LineBreakOptions, CodeMarkerOptions
+
+class SyntaxHighlighterConfig:
+ def _create_lexer_list(self):
+ lexers = []
+
+ # Iteration over get_all_lexers() seems to be broken somehow. Workarround
+ all_lexers = get_all_lexers()
+ for lexer in all_lexers:
+ # We don't want to add lexers that we cant identify by name later
+ if lexer[1] is not None and lexer[1]:
+ lexers.append((lexer[0], lexer[1][0]))
+ lexers.sort()
+ return lexers
+
+ def get_lexer_by_name(self, name):
+ lexer = None
+ try:
+ lexer = get_lexer_by_name(name)
+ except:
+ pass
+ return lexer
+
+ def get_lexer_with_fallback(self, language):
+ lexer = self.get_lexer_by_name(language)
+ if lexer is None:
+ log.info("Falling back to default lexer for %s.",
+ self.get_default_lexer_name())
+ lexer = self.default_lexer[1]
+ return lexer
+
+ def set_font(self, font):
+ if font is not None and font != "":
+ self.config['font'] = font
+
+ def set_style(self, style):
+ if style is not None and style != "":
+ self.config['style'] = style
+
+ def set_line_break_action(self, option):
+ if isinstance(option, int):
+ option = LineBreakOptions(option)
+ self.config['line_break'] = option
+
+ def set_default_lexer(self, name):
+ lexer = get_lexer_by_name(name)
+
+ if lexer is None and self.default_lexer is None:
+ log.error("Failed to get default lexer by name."\
+ "Falling back to simply using the first in the list.")
+ lexer = self.lexer_list[0]
+ name = lexer[0]
+ self.default_lexer = (name, lexer)
+ if lexer is None and self.default_lexer is not None:
+ log.info("Failed to get default lexer by name, keeping previous"\
+ "setting (lexer = %s).", self.default_lexer[0])
+ name = self.default_lexer[0]
+ else:
+ self.default_lexer = (name, lexer)
+
+ self.config['default_lexer'] = name
+
+ def set_bgcolor_override_enabled(self, state):
+ self.config['bgcolor_override'] = state
+
+ def set_bgcolor(self, color):
+ if isinstance(color, Gdk.Color):
+ color = color.to_string()
+ self.config['bgcolor'] = color
+
+ def set_code_marker_setting(self, option):
+ if isinstance(option, int):
+ option = CodeMarkerOptions(option)
+ self.config['code_marker'] = option
+
+ def set_pygments_path(self, path):
+ self.config['pygments_path'] = path
+
+ def get_default_lexer(self):
+ return self.default_lexer[1]
+
+ def get_default_lexer_name(self):
+ return self.default_lexer[0]
+
+ def get_lexer_list(self):
+ return self.lexer_list
+
+ def get_line_break_action(self):
+ # return int only
+ if isinstance(self.config['line_break'], int):
+ # in case of legacy settings, convert.
+ action = self.config['line_break']
+ self.set_line_break_action(action)
+ else:
+ action = self.config['line_break'].value
+
+ return action
+
+ def get_pygments_path(self):
+ return self.config['pygments_path']
+
+ def get_font(self):
+ return self.config['font']
+
+ def get_style_name(self):
+ return self.config['style']
+
+ def is_bgcolor_override_enabled(self):
+ return self.config['bgcolor_override']
+
+ def get_bgcolor(self):
+ return self.config['bgcolor']
+
+ def get_code_marker_setting(self):
+ return self.config['code_marker']
+
+ def get_styles_list(self):
+ return self.style_list
+
+ def init_pygments(self):
+ """
+ Initialize all config variables that depend directly on pygments being
+ available.
+ """
+ self.lexer_list = self._create_lexer_list()
+ self.style_list = [s for s in get_all_styles()]
+ self.style_list.sort()
+ self.set_default_lexer(self.config['default_lexer'])
+
+ def __init__(self, config):
+ self.lexer_list = []
+ self.style_list = []
+ self.config = config
+ self.default_lexer = None
+
+
diff --git a/syntax_highlight/plugin_config_dialog.py b/syntax_highlight/plugin_config_dialog.py
new file mode 100644
index 0000000..de58c10
--- /dev/null
+++ b/syntax_highlight/plugin_config_dialog.py
@@ -0,0 +1,170 @@
+import logging
+import pygments
+
+from gi.repository import Gtk, Gdk
+from gi.repository.Pango import FontDescription
+
+
+from gajim.plugins.gui import GajimPluginConfigDialog
+from gajim.plugins.helpers import log_calls, log
+
+
+from .gtkformatter import GTKFormatter
+from .types import MatchType, LineBreakOptions, CodeMarkerOptions
+
+log = logging.getLogger('gajim.plugin_system.syntax_highlight')
+
+class SyntaxHighlighterPluginConfiguration(GajimPluginConfigDialog):
+ @log_calls('SyntaxHighlighterPluginConfiguration')
+ def init(self):
+ self.GTK_BUILDER_FILE_PATH = self.plugin.local_file_path(
+ 'config_dialog.ui')
+ self.xml = Gtk.Builder()
+ self.xml.set_translation_domain('gajim_plugins')
+ self.xml.add_objects_from_file(self.GTK_BUILDER_FILE_PATH,
+ ['mainBox', 'line_break_selection', 'code_marker_selection',
+ 'preview_textbuffer'])
+ box = self.xml.get_object('mainBox')
+ self.get_child().pack_start(box, False, False, 0)
+ self.result_label = self.xml.get_object('result_label')
+
+ self.liststore = Gtk.ListStore(str)
+
+ self.default_lexer_combobox = self.xml.get_object('default_lexer_combobox')
+ self.default_lexer_combobox.set_property("model", self.liststore)
+
+ self.style_liststore = Gtk.ListStore(str)
+ self.style_combobox = self.xml.get_object('style_combobox')
+ self.style_combobox.set_property("model", self.style_liststore)
+
+ self.bg_color_checkbox = self.xml.get_object('bg_color_checkbutton')
+ self.bg_color_colorbutton = self.xml.get_object('bg_color_colorbutton')
+
+ self.line_break_combobox = self.xml.get_object('line_break_combobox')
+
+ self.code_marker_combobox = self.xml.get_object('code_marker_combobox')
+
+ self.preview_textview = self.xml.get_object('preview_textview')
+ self.preview_textview.get_buffer().connect("insert-text", self.on_preview_text_inserted)
+ self.preview_textview.set_size_request(-1, 130)
+
+ self.font_button = self.xml.get_object('font_button')
+
+ self.xml.connect_signals(self)
+ self.default_lexer_id = 0
+ self.style_id = 0
+
+ def set_config(self, config):
+ self.config = config
+ self.lexers = self.config.get_lexer_list()
+ self.styles = self.config.get_styles_list()
+ default_lexer = self.config.get_default_lexer_name()
+ default_style = self.config.get_style_name()
+
+ for i, lexer in enumerate(self.lexers):
+ self.liststore.append([lexer[0]])
+ if lexer[1] == default_lexer:
+ self.default_lexer_id = i
+
+ for i, style in enumerate(self.styles):
+ self.style_liststore.append([style])
+ if style == default_style:
+ self.style_id = i
+ self.update_preview()
+
+ def lexer_changed(self, _widget):
+ new = self.default_lexer_combobox.get_active()
+ if new != self.default_lexer_id:
+ self.default_lexer_id = new
+ self.config.set_default_lexer(self.lexers[self.default_lexer_id][1])
+ self.update_preview()
+
+ def line_break_changed(self, _widget):
+ new = LineBreakOptions(self.line_break_combobox.get_active())
+ if new != self.config.get_line_break_action():
+ self.config.set_line_break_action(new)
+ self.update_preview()
+
+ def code_marker_changed(self, _widget):
+ new = CodeMarkerOptions(self.code_marker_combobox.get_active())
+ if new != self.config.get_code_marker_setting():
+ self.config.set_code_marker_setting(new)
+
+ def bg_color_enabled(self, _widget):
+ new = self.bg_color_checkbox.get_active()
+ if new != self.config.is_bgcolor_override_enabled():
+ bg_override_enabled = new
+ self.config.set_bgcolor_override_enabled(bg_override_enabled)
+ self.bg_color_colorbutton.set_sensitive(bg_override_enabled)
+ self.update_preview()
+
+ def bg_color_changed(self, _widget):
+ new = self.bg_color_colorbutton.get_color()
+ if new != self.config.get_bgcolor():
+ self.config.set_bgcolor(new)
+ self.update_preview()
+
+ def style_changed(self, _widget):
+ new = self.style_combobox.get_active()
+ if new != self.style_id:
+ self.style_id = new
+ self.config.set_style(self.styles[self.style_id])
+ self.update_preview()
+
+ def font_changed(self, _widget):
+ new = self.font_button.get_font()
+ if new != self.config.get_font():
+ self.config.set_font(new)
+ self.update_preview()
+
+ def update_preview(self):
+ self.format_preview_text()
+
+ def on_preview_text_inserted(self, _buf, _iterator, text, length, *_args):
+ if (length == 1 and re.match(r'\s', text)) or length > 1:
+ self.format_preview_text()
+
+ def format_preview_text(self):
+ buf = self.preview_textview.get_buffer()
+ start_iter = buf.get_start_iter()
+ start_mark = buf.create_mark(None, start_iter, True)
+ buf.remove_all_tags(start_iter, buf.get_end_iter())
+
+ formatter = GTKFormatter(
+ style=self.config.get_style_name(),
+ start_mark=start_mark)
+
+ code = start_iter.get_text(buf.get_end_iter())
+ lexer = self.config.get_default_lexer()
+ tokens = pygments.lex(code, lexer)
+
+ pygments.format(tokens, formatter, buf)
+
+ buf.delete_mark(start_mark)
+
+ self.preview_textview.override_font(
+ FontDescription.from_string(self.config.get_font()))
+
+ color = Gdk.RGBA()
+ if color.parse(self.config.get_bgcolor()):
+ self.preview_textview.override_background_color(
+ Gtk.StateFlags.NORMAL, color)
+
+ def on_run(self):
+ self.default_lexer_combobox.set_active(self.default_lexer_id)
+ self.line_break_combobox.set_active(self.config.get_line_break_action())
+ self.code_marker_combobox.set_active(self.config.get_code_marker_setting())
+ self.style_combobox.set_active(self.style_id)
+
+ self.font_button.set_font(self.config.get_font())
+
+ bg_override_enabled = self.config.is_bgcolor_override_enabled()
+ self.bg_color_checkbox.set_active(bg_override_enabled)
+
+ self.bg_color_colorbutton.set_sensitive(bg_override_enabled)
+
+ parsed, color = Gdk.Color.parse(self.config.get_bgcolor())
+ if parsed:
+ self.bg_color_colorbutton.set_color(color)
+
+
diff --git a/syntax_highlight/syntax_highlight.py b/syntax_highlight/syntax_highlight.py
index f55c3c1..6de50ae 100644
--- a/syntax_highlight/syntax_highlight.py
+++ b/syntax_highlight/syntax_highlight.py
@@ -3,16 +3,10 @@ import re
import sys
import importlib
-from enum import Enum, IntEnum, unique
-
-from gi.repository import Gtk, Gdk
-from gi.repository.Gtk import TextSearchFlags
-from gi.repository.Pango import FontDescription
-
from gajim.plugins.helpers import log_calls, log
from gajim.plugins import GajimPlugin
-from gajim.plugins.gui import GajimPluginConfigDialog
+from .types import MatchType, LineBreakOptions, CodeMarkerOptions
log = logging.getLogger('gajim.plugin_system.syntax_highlight')
@@ -20,11 +14,10 @@ def try_loading_pygments():
success = importlib.find_loader('pygments') is not None
if success:
try:
- import pygments
- from pygments.lexers import get_lexer_by_name, get_all_lexers
- from pygments.styles import get_all_styles
- from .gtkformatter import GTKFormatter
- global pygments, get_lexer_by_name, get_all_lexers, get_all_styles, GTKFormatter
+ from .chat_syntax_highlighter import ChatSyntaxHighlighter
+ from .plugin_config_dialog import SyntaxHighlighterPluginConfiguration
+ from .plugin_config import SyntaxHighlighterConfig
+ global SyntaxHighlighterPluginConfiguration, ChatSyntaxHighlighter, SyntaxHighlighterConfig
success = True
log.debug("pygments loaded.")
except Exception as exception:
@@ -33,569 +26,11 @@ def try_loading_pygments():
return success
-class MatchType(Enum):
- INLINE = 0
- MULTILINE = 1
- TEXT = 2
-
-@unique
-class LineBreakOptions(IntEnum):
- NEVER = 0
- ALWAYS = 1
- MULTILINE = 2
-
-@unique
-class CodeMarkerOptions(IntEnum):
- AS_COMMENT = 0
- HIDE = 1
-
PYGMENTS_MISSING = 'You are missing Python-Pygments.'
-
-
-
-class SyntaxHighlighterPluginConfiguration(GajimPluginConfigDialog):
- @log_calls('SyntaxHighlighterPluginConfiguration')
- def init(self):
- self.GTK_BUILDER_FILE_PATH = self.plugin.local_file_path(
- 'config_dialog.ui')
- self.xml = Gtk.Builder()
- self.xml.set_translation_domain('gajim_plugins')
- self.xml.add_objects_from_file(self.GTK_BUILDER_FILE_PATH,
- ['mainBox', 'line_break_selection', 'code_marker_selection',
- 'preview_textbuffer'])
- box = self.xml.get_object('mainBox')
- self.get_child().pack_start(box, False, False, 0)
- self.result_label = self.xml.get_object('result_label')
-
- self.liststore = Gtk.ListStore(str)
-
- self.default_lexer_combobox = self.xml.get_object('default_lexer_combobox')
- self.default_lexer_combobox.set_property("model", self.liststore)
-
- self.style_liststore = Gtk.ListStore(str)
- self.style_combobox = self.xml.get_object('style_combobox')
- self.style_combobox.set_property("model", self.style_liststore)
-
- self.bg_color_checkbox = self.xml.get_object('bg_color_checkbutton')
- self.bg_color_colorbutton = self.xml.get_object('bg_color_colorbutton')
-
- self.line_break_combobox = self.xml.get_object('line_break_combobox')
-
- self.code_marker_combobox = self.xml.get_object('code_marker_combobox')
-
- self.preview_textview = self.xml.get_object('preview_textview')
- self.preview_textview.get_buffer().connect("insert-text", self.on_preview_text_inserted)
- self.preview_textview.set_size_request(-1, 130)
-
- self.font_button = self.xml.get_object('font_button')
-
- self.xml.connect_signals(self)
- self.default_lexer_id = 0
- self.style_id = 0
-
- def set_config(self, config):
- self.config = config
- self.lexers = self.config.get_lexer_list()
- self.styles = self.config.get_styles_list()
- default_lexer = self.config.get_default_lexer_name()
- default_style = self.config.get_style_name()
-
- for i, lexer in enumerate(self.lexers):
- self.liststore.append([lexer[0]])
- if lexer[1] == default_lexer:
- self.default_lexer_id = i
-
- for i, style in enumerate(self.styles):
- self.style_liststore.append([style])
- if style == default_style:
- self.style_id = i
- self.update_preview()
-
- def lexer_changed(self, _widget):
- new = self.default_lexer_combobox.get_active()
- if new != self.default_lexer_id:
- self.default_lexer_id = new
- self.config.set_default_lexer(self.lexers[self.default_lexer_id][1])
- self.update_preview()
-
- def line_break_changed(self, _widget):
- new = LineBreakOptions(self.line_break_combobox.get_active())
- if new != self.config.get_line_break_action():
- self.config.set_line_break_action(new)
- self.update_preview()
-
- def code_marker_changed(self, _widget):
- new = CodeMarkerOptions(self.code_marker_combobox.get_active())
- if new != self.config.get_code_marker_setting():
- self.config.set_code_marker_setting(new)
-
- def bg_color_enabled(self, _widget):
- new = self.bg_color_checkbox.get_active()
- if new != self.config.is_bgcolor_override_enabled():
- bg_override_enabled = new
- self.config.set_bgcolor_override_enabled(bg_override_enabled)
- self.bg_color_colorbutton.set_sensitive(bg_override_enabled)
- self.update_preview()
-
- def bg_color_changed(self, _widget):
- new = self.bg_color_colorbutton.get_color()
- if new != self.config.get_bgcolor():
- self.config.set_bgcolor(new)
- self.update_preview()
-
- def style_changed(self, _widget):
- new = self.style_combobox.get_active()
- if new != self.style_id:
- self.style_id = new
- self.config.set_style(self.styles[self.style_id])
- self.update_preview()
-
- def font_changed(self, _widget):
- new = self.font_button.get_font()
- if new != self.config.get_font():
- self.config.set_font(new)
- self.update_preview()
-
- def update_preview(self):
- self.format_preview_text()
-
- def on_preview_text_inserted(self, _buf, _iterator, text, length, *_args):
- if (length == 1 and re.match(r'\s', text)) or length > 1:
- self.format_preview_text()
-
- def format_preview_text(self):
- buf = self.preview_textview.get_buffer()
- start_iter = buf.get_start_iter()
- start_mark = buf.create_mark(None, start_iter, True)
- buf.remove_all_tags(start_iter, buf.get_end_iter())
-
- formatter = GTKFormatter(
- style=self.config.get_style_name(),
- start_mark=start_mark)
-
- code = start_iter.get_text(buf.get_end_iter())
- lexer = self.config.get_default_lexer()
- tokens = pygments.lex(code, lexer)
-
- pygments.format(tokens, formatter, buf)
-
- buf.delete_mark(start_mark)
-
- self.preview_textview.override_font(
- FontDescription.from_string(self.config.get_font()))
-
- color = Gdk.RGBA()
- if color.parse(self.config.get_bgcolor()):
- self.preview_textview.override_background_color(
- Gtk.StateFlags.NORMAL, color)
-
- def on_run(self):
- self.default_lexer_combobox.set_active(self.default_lexer_id)
- self.line_break_combobox.set_active(self.config.get_line_break_action())
- self.code_marker_combobox.set_active(self.config.get_code_marker_setting())
- self.style_combobox.set_active(self.style_id)
-
- self.font_button.set_font(self.config.get_font())
-
- bg_override_enabled = self.config.is_bgcolor_override_enabled()
- self.bg_color_checkbox.set_active(bg_override_enabled)
-
- self.bg_color_colorbutton.set_sensitive(bg_override_enabled)
-
- parsed, color = Gdk.Color.parse(self.config.get_bgcolor())
- if parsed:
- self.bg_color_colorbutton.set_color(color)
-
-
-
-class ChatSyntaxHighlighter:
- def hide_code_markup(self, buf, start, end):
- tag = buf.get_tag_table().lookup('hide_code_markup')
- if tag is None:
- tag = Gtk.TextTag.new('hide_code_markup')
- tag.set_property('invisible', True)
- buf.get_tag_table().add(tag)
-
- buf.apply_tag_by_name('hide_code_markup', start, end)
-
- def check_line_break(self, is_multiline):
- line_break = self.config.get_line_break_action()
-
- return (line_break == LineBreakOptions.ALWAYS) \
- or (is_multiline and line_break == LineBreakOptions.MULTILINE)
-
-
- def format_code(self, buf, s_tag, s_code, e_tag, e_code, language):
- style = self.config.get_style_name()
- if self.config.get_code_marker_setting() == CodeMarkerOptions.HIDE:
- self.hide_code_markup(buf, s_tag, s_code)
- self.hide_code_markup(buf, e_code, e_tag)
- else:
- comment_tag = GTKFormatter.create_tag_for_token(
- pygments.token.Comment,
- pygments.styles.get_style_by_name(style))
- buf.get_tag_table().add(comment_tag)
- buf.apply_tag(comment_tag, s_tag, s_code)
- buf.apply_tag(comment_tag, e_tag, e_code)
-
- code = s_code.get_text(e_code)
- log.debug("full text to encode: %s.", code)
-
-
- start_mark = buf.create_mark(None, s_code, False)
-
- lexer = None
-
- if language is None:
- lexer = self.config.get_default_lexer()
- log.info("No Language specified. Falling back to default lexer: %s.",
- self.config.get_default_lexer_name())
- else:
- log.debug("Using lexer for %s.", str(language))
- lexer = self.config.get_lexer_with_fallback(language)
-
- if lexer is None:
- iterator = buf.get_iter_at_mark(start_mark)
- buf.insert(iterator, '\n')
- else:
- tokens = pygments.lex(code, lexer)
-
- formatter = GTKFormatter(style=style, start_mark=start_mark)
- pygments.format(tokens, formatter, buf)
-
- def find_multiline_matches(self, text):
- start = None
- matches = []
- for i in re.finditer(r'\n?```(?:\S*\n)?', text, re.DOTALL):
- if start is None:
- start = i
- elif re.match(r'^\n```', i.group(0)) is not None:
- matches.append(
- (start.start(), i.end(), text[start.start():i.end()]))
- start = None
- else:
- # not an end...
- continue
- return matches
-
- def find_inline_matches(self, text):
- return [(i.start(), i.end(), i.group(0)) for i in \
- re.finditer(r'(?<!`)`(?!`|\n).+(?<!`)`', text)]
-
- def merge_match_groups(self, real_text, inline_matches, multiline_matches):
- it_inline = iter(inline_matches)
- it_multi = iter(multiline_matches)
- length = len(real_text)
-
- # Just to get cleaner code below...
- def get_next(iterator):
- return next(iterator, (length, length, ""))
-
- # In order to simplify the process, we use the 'length' here.
- cur_inline = get_next(it_inline)
- cur_multi = get_next(it_multi)
-
- pos = 0
-
- # This will contain tuples with parts of the input and its classification
- parts = []
- while pos < length:
- log.debug("-> in: %s", str(cur_inline))
- log.debug("-> mu: %s", str(cur_multi))
-
- # selected = (start, end, type)
- selected = (cur_inline[0], cur_inline[1], MatchType.INLINE) \
- if cur_inline[0] < cur_multi[0] \
- else (cur_multi[0], cur_multi[1], MatchType.MULTILINE) \
- if cur_multi[0] < length \
- else (pos, length, MatchType.TEXT)
- log.debug("--> select: %s", str(selected))
-
- # Handle plain text string parts (and unforseen errors...)
- if pos < selected[0]:
- end = selected[0] if selected[0] != pos else selected[1]
- parts.append((real_text[pos:end], MatchType.TEXT))
- pos = selected[0]
- elif pos > selected[0]:
- log.error("Should not happen, position > found match.")
-
- # Cut out and append selected text segment
- parts.append((real_text[selected[0]:selected[1]], selected[2]))
- pos = selected[1]
-
- # Depending on the match type, we have to forward the iterators.
- # Also, forward the other one, if regions overlap or we took over...
- if selected[2] == MatchType.INLINE:
- if cur_multi[0] < cur_inline[1]:
- cur_multi = get_next(it_multi)
- cur_inline = get_next(it_inline)
- elif selected[2] == MatchType.MULTILINE:
- if cur_inline[0] < cur_multi[1]:
- cur_inline = get_next(it_inline)
- cur_multi = get_next(it_multi)
-
- return parts
-
- def process_text(self, real_text, other_tags, _graphics, iter_,
- _additional):
- def fix_newline(char, marker_len_no_newline, force=False):
- fixed = (marker_len_no_newline, '')
- if char == '\n':
- fixed = (marker_len_no_newline + 1, '')
- elif force:
- fixed = (marker_len_no_newline + 1, '\n')
- return fixed
-
-
- buf = self.textview.tv.get_buffer()
-
- # first, try to find inline or multiline code snippets
- inline_matches = self.find_inline_matches(real_text)
- multiline_matches = self.find_multiline_matches(real_text)
-
- if not inline_matches and not multiline_matches:
- log.debug("Stopping early, since there is no code block in it....")
- return
-
- iterator = iter_ if iter_ is not None else buf.get_end_iter()
-
- # Create a start marker with left gravity before inserting text.
- start_mark = buf.create_mark("SHP_start", iterator, True)
- end_mark = buf.create_mark("SHP_end", iterator, False)
-
- insert_newline_for_multiline = self.check_line_break(True)
- insert_newline_for_inline = self.check_line_break(False)
-
- split_text = self.merge_match_groups(
- real_text, inline_matches, multiline_matches)
-
- buf.begin_user_action()
-
- for num, (text_to_insert, match_type) in enumerate(split_text):
- marker = [("", 0), ("", 0)]
- language = None
- end_of_message = num == (len(split_text) - 1)
-
- if match_type == MatchType.TEXT:
- self.textview.detect_and_print_special_text(
- text_to_insert, other_tags, graphics=_graphics,
- iter_=iterator, additional_data=_additional)
- else:
- if match_type == MatchType.MULTILINE:
- language_match = re.search(
- '\n*```([^\n]*)\n', text_to_insert, re.DOTALL)
- language = None if language_match is None \
- else language_match.group(1)
- language_len = 0 if language is None else len(language)
-
- # We account the language word width for the front marker
- front = fix_newline(text_to_insert[0], 3 + language_len,
- insert_newline_for_multiline)
- back = fix_newline(text_to_insert[-1], 3,
- insert_newline_for_multiline and not end_of_message)
- else:
- front = fix_newline(text_to_insert[0], 1,
- insert_newline_for_inline)
- back = fix_newline(text_to_insert[-1], 1,
- insert_newline_for_inline and not end_of_message)
-
- marker_widths = (front[0], back[0])
- text_to_insert = ''.join([front[1], text_to_insert, back[1]])
-
- # insertion invalidates iterator, let's use our start mark...
- self.insert_and_format_code(buf, text_to_insert, language,
- marker_widths, start_mark, other_tags)
-
- iterator = buf.get_iter_at_mark(end_mark)
- # the current end of the buffer's contents is the start for the
- # next iteration
- buf.move_mark(start_mark, iterator)
-
- buf.delete_mark(start_mark)
- buf.delete_mark(end_mark)
-
- buf.end_user_action()
-
- # We have to make sure this is the last thing we do (i.e. no calls to
- # the other textview methods no more from here on), because the
- # print_special_text method is resetting the plugin_modified variable...
- self.textview.plugin_modified = True
-
- def insert_and_format_code(self, buf, insert_text, language, marker, start_mark, other_tags=None):
- start_iter = buf.get_iter_at_mark(start_mark)
-
- if other_tags:
- buf.insert_with_tags_by_name(start_iter, insert_text,
- *other_tags)
- else:
- buf.insert(start_iter, insert_text)
-
- start_iter = buf.get_iter_at_mark(start_mark)
- tag_start = start_iter
- tag_end = buf.get_end_iter()
- s_code = start_iter.copy()
- e_code = tag_end.copy()
- s_code.forward_chars(marker[0])
- e_code.backward_chars(marker[1])
-
- log.debug("full text between tags: %s.", tag_start.get_text(tag_end))
-
- self.format_code(buf, tag_start, s_code, tag_end, e_code, language)
-
- self.textview.plugin_modified = True
-
- # Set general code block format
- tag = Gtk.TextTag.new()
- if self.config.is_bgcolor_override_enabled():
- tag.set_property('background', self.config.get_bgcolor())
- tag.set_property('paragraph-background', self.config.get_bgcolor())
- tag.set_property('font', self.config.get_font())
- buf.get_tag_table().add(tag)
- buf.apply_tag(tag, start_iter, buf.get_end_iter())
-
- def __init__(self, config, textview):
- self.last_end_mark = None
- self.config = config
- self.textview = textview
-
-class SyntaxHighlighterConfig:
- def _create_lexer_list(self):
- lexers = []
-
- # Iteration over get_all_lexers() seems to be broken somehow. Workarround
- all_lexers = get_all_lexers()
- for lexer in all_lexers:
- # We don't want to add lexers that we cant identify by name later
- if lexer[1] is not None and lexer[1]:
- lexers.append((lexer[0], lexer[1][0]))
- lexers.sort()
- return lexers
-
- def get_lexer_by_name(self, name):
- lexer = None
- try:
- lexer = get_lexer_by_name(name)
- except:
- pass
- return lexer
-
- def get_lexer_with_fallback(self, language):
- lexer = self.get_lexer_by_name(language)
- if lexer is None:
- log.info("Falling back to default lexer for %s.",
- self.get_default_lexer_name())
- lexer = self.default_lexer[1]
- return lexer
-
- def set_font(self, font):
- if font is not None and font != "":
- self.config['font'] = font
-
- def set_style(self, style):
- if style is not None and style != "":
- self.config['style'] = style
-
- def set_line_break_action(self, option):
- if isinstance(option, int):
- option = LineBreakOptions(option)
- self.config['line_break'] = option
-
- def set_default_lexer(self, name):
- lexer = get_lexer_by_name(name)
-
- if lexer is None and self.default_lexer is None:
- log.error("Failed to get default lexer by name."\
- "Falling back to simply using the first in the list.")
- lexer = self.lexer_list[0]
- name = lexer[0]
- self.default_lexer = (name, lexer)
- if lexer is None and self.default_lexer is not None:
- log.info("Failed to get default lexer by name, keeping previous"\
- "setting (lexer = %s).", self.default_lexer[0])
- name = self.default_lexer[0]
- else:
- self.default_lexer = (name, lexer)
-
- self.config['default_lexer'] = name
-
- def set_bgcolor_override_enabled(self, state):
- self.config['bgcolor_override'] = state
-
- def set_bgcolor(self, color):
- if isinstance(color, Gdk.Color):
- color = color.to_string()
- self.config['bgcolor'] = color
-
- def set_code_marker_setting(self, option):
- if isinstance(option, int):
- option = CodeMarkerOptions(option)
- self.config['code_marker'] = option
-
- def set_pygments_path(self, path):
- self.config['pygments_path'] = path
-
- def get_default_lexer(self):
- return self.default_lexer[1]
-
- def get_default_lexer_name(self):
- return self.default_lexer[0]
-
- def get_lexer_list(self):
- return self.lexer_list
-
- def get_line_break_action(self):
- # return int only
- if isinstance(self.config['line_break'], int):
- # in case of legacy settings, convert.
- action = self.config['line_break']
- self.set_line_break_action(action)
- else:
- action = self.config['line_break'].value
-
- return action
-
- def get_pygments_path(self):
- return self.config['pygments_path']
-
- def get_font(self):
- return self.config['font']
-
- def get_style_name(self):
- return self.config['style']
-
- def is_bgcolor_override_enabled(self):
- return self.config['bgcolor_override']
-
- def get_bgcolor(self):
- return self.config['bgcolor']
-
- def get_code_marker_setting(self):
- return self.config['code_marker']
-
- def get_styles_list(self):
- return self.style_list
-
- def init_pygments(self):
- """
- Initialize all config variables that depend directly on pygments being
- available.
- """
- self.lexer_list = self._create_lexer_list()
- self.style_list = [s for s in get_all_styles()]
- self.style_list.sort()
- self.set_default_lexer(self.config['default_lexer'])
-
- def __init__(self, config):
- self.lexer_list = []
- self.style_list = []
- self.config = config
- self.default_lexer = None
-
class SyntaxHighlighterPlugin(GajimPlugin):
-
@log_calls('SyntaxHighlighterPlugin')
def on_connect_with_chat_control(self, chat_control):
account = chat_control.contact.account.name
@@ -637,6 +72,7 @@ class SyntaxHighlighterPlugin(GajimPlugin):
self.available_text = None
self.config_dialog = SyntaxHighlighterPluginConfiguration(self)
+ self.conf = SyntaxHighlighterConfig(self.config)
# The following initialization requires pygments to be available.
self.conf.init_pygments()
@@ -666,7 +102,6 @@ class SyntaxHighlighterPlugin(GajimPlugin):
'code_marker' : (CodeMarkerOptions.AS_COMMENT, ''),
'pygments_path' : (None, ''),
}
- self.conf = SyntaxHighlighterConfig(self.config)
is_initialized = self.try_init()
diff --git a/syntax_highlight/types.py b/syntax_highlight/types.py
new file mode 100644
index 0000000..3f9fd56
--- /dev/null
+++ b/syntax_highlight/types.py
@@ -0,0 +1,19 @@
+from enum import Enum, IntEnum, unique
+
+class MatchType(Enum):
+ INLINE = 0
+ MULTILINE = 1
+ TEXT = 2
+
+@unique
+class LineBreakOptions(IntEnum):
+ NEVER = 0
+ ALWAYS = 1
+ MULTILINE = 2
+
+@unique
+class CodeMarkerOptions(IntEnum):
+ AS_COMMENT = 0
+ HIDE = 1
+
+