[syntax_highlight] Simplify plugin code

Fix deprecations warnings in config
This commit is contained in:
Daniel Brötzmann
2020-04-16 18:13:30 +02:00
parent 68d78970fb
commit 425b1bd2cd
9 changed files with 429 additions and 583 deletions

View File

@@ -1,123 +0,0 @@
# Syntax Highlighting Plugin for Gajim
[Gajim](https://gajim.org) Plugin that highlights source code blocks in the chat window.
## Installation
The recommended way of installing this plugin is to use Gajim's Plugin Installer.
For more information and instruction on how to install plugins manually, please
refer to the [Gajim Plugin Wiki site](https://dev.gajim.org/gajim/gajim-plugins/wikis/home#how-to-install-plugins).
## Usage
This plugin uses markdown-style syntax to identify which parts of a message
should be formatted as code in the chat window.
```
Inline source code will be highlighted when placed in between `two single
back-ticks`.
```
The language used to highlight the syntax of inline code is selected as the
default language in the plugin settings.
Multi-line code blocks are started by three back-ticks followed by a newline.
Optionally, a language can be specified directly after the opening back-ticks and
before the line break:
````
```language
Note, that the last line of a code block may only contain the closing back-ticks,
i.e. there must be a newline here.
```
````
In case no language is specified with the opening tag or the specified language
could not be identified, the default language configured in the settings is
used.
You can test it by copying and sending the following text to one of your
contacts:
````
```python
def test():
print("Hello, world!")
```
````
(**Note:** your contact will not receive highlighted text unless she is also
using the plugin.)
## Relation to XEP-0393 - 'Message Styling'
https://xmpp.org/extensions/xep-0393.html#pre-block
In [XEP-0393](https://xmpp.org/extensions/xep-0393.html),
the back-tick based syntax is defined as markup for preformatted
text blocks, respectively inline preformatted text.
Formatting of such text blocks with mono-spaced fonts is recommended by the XEP.
By using the same syntax as defined in XEP-0393 XMPP clients with only XEP-0393
support but without syntax highlighting can at least present their users blocks
of preformatted text.
Since text in between the back-tick markers is not further formatted by this
plugin, it can be considered "preformatted".
Hence, this plugin is compatible to the formatting options defined by XEP-0393,
[section 5.1.2, "Preformatted Text"](https://xmpp.org/extensions/xep-0393.html#pre-block)
and [section 5.2.5, "Preformatted Span"](https://xmpp.org/extensions/xep-0393.html#mono).
Nevertheless, syntax highlighting for source code is not part of XEP but
rather a non-standard extension introduced with this plugin.
## Configuration
The configuration can be found via 'Gajim' > 'Plugins', then select the
'Source Code Syntax Highlight' Plugin and click the gears symbol.
The configuration options let you specify many details how code is formatted,
including default language, style, font settings, background color and formatting
of the code markers.
In the configuration window, the current settings are displayed in an
interactive preview panel. This allows you to directly check how code would
look like in the message
window.
## Report Bugs and Feature Requests
For bug reports, please report them to the [Gajim Plugin Issue tracker](https://dev.gajim.org/gajim/gajim-plugins/issues/new?issue[FlorianMuenchbach]=&issue[description]=Gajim%20Version%3A%20%0APlugin%20Version%3A%0AOperating%20System%3A&issue[title]=[syntax_highlight]).
Please make sure that the issue you create contains `[syntax_highlight]` in the
title and information such as Gajim version, Plugin version, Operating system,
etc.
## Debug
The plugin adds its own logger. It can be used to set a specific debug level
for this plugin and/or filter log messages.
Run
```
gajim --loglevel gajim.p.syntax_highlight=DEBUG
```
in a terminal to display the debug messages.
## Known Issues / ToDo
* ~~Gajim crashes when correcting a message containing highlighted code.~~
(fixed in version 1.1.0)
## Credits
Since I had no experience in writing Plugins for Gajim, I used the
[Latex Plugin](https://dev.gajim.org/gajim/gajim-plugins/wikis/LatexPlugin)
written by Yves Fischer and Yann Leboulanger as an example and copied a big
portion of initial code. Therefore, credits go to the authors of the Latex
Plugin for providing an example.
The syntax highlighting itself is done by [pygments](http://pygments.org/).

View File

@@ -8,12 +8,22 @@ from syntax_highlight.gtkformatter import GTKFormatter
from syntax_highlight.types import MatchType from syntax_highlight.types import MatchType
from syntax_highlight.types import LineBreakOptions from syntax_highlight.types import LineBreakOptions
from syntax_highlight.types import CodeMarkerOptions from syntax_highlight.types import CodeMarkerOptions
from syntax_highlight.types import PLUGIN_INTERNAL_NONE_LEXER_ID
log = logging.getLogger('gajim.p.syntax_highlight') log = logging.getLogger('gajim.p.syntax_highlight')
class ChatSyntaxHighlighter: class ChatSyntaxHighlighter:
def hide_code_markup(self, buf, start, end): def __init__(self, plugin_config, highlighter_config, textview):
self.textview = textview
self._plugin_config = plugin_config
self._highlighter_config = highlighter_config
def update_config(self, plugin_config):
self._plugin_config = plugin_config
@staticmethod
def _hide_code_markup(buf, start, end):
tag = buf.get_tag_table().lookup('hide_code_markup') tag = buf.get_tag_table().lookup('hide_code_markup')
if tag is None: if tag is None:
tag = Gtk.TextTag.new('hide_code_markup') tag = Gtk.TextTag.new('hide_code_markup')
@@ -22,17 +32,16 @@ class ChatSyntaxHighlighter:
buf.apply_tag_by_name('hide_code_markup', start, end) buf.apply_tag_by_name('hide_code_markup', start, end)
def check_line_break(self, is_multiline): def _check_line_break(self, is_multiline):
line_break = self.config.get_line_break_action() line_break = self._plugin_config['line_break'].value
return (line_break == LineBreakOptions.ALWAYS) \ return (line_break == LineBreakOptions.ALWAYS) \
or (is_multiline and line_break == LineBreakOptions.MULTILINE) or (is_multiline and line_break == LineBreakOptions.MULTILINE)
def format_code(self, buf, s_tag, s_code, e_tag, e_code, language): def _format_code(self, buf, s_tag, s_code, e_tag, e_code, language):
style = self.config.get_style_name() style = self._plugin_config['style']
if self.config.get_code_marker_setting() == CodeMarkerOptions.HIDE: if self._plugin_config['code_marker'] == CodeMarkerOptions.HIDE:
self.hide_code_markup(buf, s_tag, s_code) self._hide_code_markup(buf, s_tag, s_code)
self.hide_code_markup(buf, e_code, e_tag) self._hide_code_markup(buf, e_code, e_tag)
else: else:
comment_tag = GTKFormatter.create_tag_for_token( comment_tag = GTKFormatter.create_tag_for_token(
pygments.token.Comment, pygments.token.Comment,
@@ -49,24 +58,25 @@ class ChatSyntaxHighlighter:
lexer = None lexer = None
if language is None: if language is None:
lexer = self.config.get_default_lexer() lexer = self._highlighter_config.get_default_lexer()
log.info('No Language specified. ' log.info('No Language specified. '
'Falling back to default lexer: %s.', 'Falling back to default lexer: %s.',
self.config.get_default_lexer_name()) self._highlighter_config.get_default_lexer_name())
else: else:
log.debug('Using lexer for %s.', str(language)) log.debug('Using lexer for %s.', str(language))
lexer = self.config.get_lexer_with_fallback(language) lexer = self._highlighter_config.get_lexer_with_fallback(language)
if lexer is None: if lexer is None:
iterator = buf.get_iter_at_mark(start_mark) iterator = buf.get_iter_at_mark(start_mark)
buf.insert(iterator, '\n') buf.insert(iterator, '\n')
elif not self.config.is_internal_none_lexer(lexer): elif lexer != PLUGIN_INTERNAL_NONE_LEXER_ID:
tokens = pygments.lex(code, lexer) tokens = pygments.lex(code, lexer)
formatter = GTKFormatter(style=style, start_mark=start_mark) formatter = GTKFormatter(style=style, start_mark=start_mark)
pygments.format(tokens, formatter, buf) pygments.format(tokens, formatter, buf)
def find_multiline_matches(self, text): @staticmethod
def _find_multiline_matches(text):
start = None start = None
matches = [] matches = []
# Less strict, allow prefixed whitespaces: # Less strict, allow prefixed whitespaces:
@@ -84,7 +94,8 @@ class ChatSyntaxHighlighter:
continue continue
return matches return matches
def find_inline_matches(self, text): @staticmethod
def _find_inline_matches(text):
""" """
Inline code is highlighted if the start marker is precedded by a start Inline code is highlighted if the start marker is precedded by a start
of line, a whitespace character or either of the other span markers of line, a whitespace character or either of the other span markers
@@ -95,18 +106,19 @@ class ChatSyntaxHighlighter:
re.finditer(r'(?:^|\s|\*|~|_)(`((?!`).+?)`)(?:\s|\*|~|_|$)', re.finditer(r'(?:^|\s|\*|~|_)(`((?!`).+?)`)(?:\s|\*|~|_|$)',
text)] text)]
def merge_match_groups(self, real_text, inline_matches, multiline_matches): @staticmethod
def _merge_match_groups(real_text, inline_matches, multiline_matches):
it_inline = iter(inline_matches) it_inline = iter(inline_matches)
it_multi = iter(multiline_matches) it_multi = iter(multiline_matches)
length = len(real_text) length = len(real_text)
# Just to get cleaner code below... # Just to get cleaner code below...
def get_next(iterator): def _get_next(iterator):
return next(iterator, (length, length, '')) return next(iterator, (length, length, ''))
# In order to simplify the process, we use the 'length' here. # In order to simplify the process, we use the 'length' here.
cur_inline = get_next(it_inline) cur_inline = _get_next(it_inline)
cur_multi = get_next(it_multi) cur_multi = _get_next(it_multi)
pos = 0 pos = 0
@@ -142,18 +154,18 @@ class ChatSyntaxHighlighter:
# Also, forward the other one, if regions overlap or we took over... # Also, forward the other one, if regions overlap or we took over...
if selected[2] == MatchType.INLINE: if selected[2] == MatchType.INLINE:
if cur_multi[0] < cur_inline[1]: if cur_multi[0] < cur_inline[1]:
cur_multi = get_next(it_multi) cur_multi = _get_next(it_multi)
cur_inline = get_next(it_inline) cur_inline = _get_next(it_inline)
elif selected[2] == MatchType.MULTILINE: elif selected[2] == MatchType.MULTILINE:
if cur_inline[0] < cur_multi[1]: if cur_inline[0] < cur_multi[1]:
cur_inline = get_next(it_inline) cur_inline = _get_next(it_inline)
cur_multi = get_next(it_multi) cur_multi = _get_next(it_multi)
return parts return parts
def process_text(self, real_text, other_tags, _graphics, iter_, def process_text(self, real_text, other_tags, _graphics, iter_,
_additional): _additional):
def fix_newline(char, marker_len_no_newline, force=False): def _fix_newline(char, marker_len_no_newline, force=False):
fixed = (marker_len_no_newline, '') fixed = (marker_len_no_newline, '')
if char == '\n': if char == '\n':
fixed = (marker_len_no_newline + 1, '') fixed = (marker_len_no_newline + 1, '')
@@ -164,8 +176,8 @@ class ChatSyntaxHighlighter:
buf = self.textview.tv.get_buffer() buf = self.textview.tv.get_buffer()
# First, try to find inline or multiline code snippets # First, try to find inline or multiline code snippets
inline_matches = self.find_inline_matches(real_text) inline_matches = self._find_inline_matches(real_text)
multiline_matches = self.find_multiline_matches(real_text) multiline_matches = self._find_multiline_matches(real_text)
if not inline_matches and not multiline_matches: if not inline_matches and not multiline_matches:
log.debug('Stopping early, since there is no code block in it...') log.debug('Stopping early, since there is no code block in it...')
@@ -177,10 +189,10 @@ class ChatSyntaxHighlighter:
start_mark = buf.create_mark('SHP_start', iterator, True) start_mark = buf.create_mark('SHP_start', iterator, True)
end_mark = buf.create_mark('SHP_end', iterator, False) end_mark = buf.create_mark('SHP_end', iterator, False)
insert_newline_for_multiline = self.check_line_break(True) insert_newline_for_multiline = self._check_line_break(True)
insert_newline_for_inline = self.check_line_break(False) insert_newline_for_inline = self._check_line_break(False)
split_text = self.merge_match_groups( split_text = self._merge_match_groups(
real_text, inline_matches, multiline_matches) real_text, inline_matches, multiline_matches)
buf.begin_user_action() buf.begin_user_action()
@@ -204,20 +216,20 @@ class ChatSyntaxHighlighter:
language_len = 0 if language is None else len(language) language_len = 0 if language is None else len(language)
# We account the language word width for the front marker # We account the language word width for the front marker
front = fix_newline( front = _fix_newline(
text_to_insert[0], text_to_insert[0],
3 + language_len, 3 + language_len,
insert_newline_for_multiline) insert_newline_for_multiline)
back = fix_newline( back = _fix_newline(
text_to_insert[-1], text_to_insert[-1],
3, 3,
insert_newline_for_multiline and not end_of_message) insert_newline_for_multiline and not end_of_message)
else: else:
front = fix_newline( front = _fix_newline(
text_to_insert[0], text_to_insert[0],
1, 1,
insert_newline_for_inline) insert_newline_for_inline)
back = fix_newline( back = _fix_newline(
text_to_insert[-1], text_to_insert[-1],
1, 1,
insert_newline_for_inline and not end_of_message) insert_newline_for_inline and not end_of_message)
@@ -226,8 +238,9 @@ class ChatSyntaxHighlighter:
text_to_insert = ''.join([front[1], text_to_insert, back[1]]) text_to_insert = ''.join([front[1], text_to_insert, back[1]])
# Insertion invalidates iterator, let's use our start mark... # Insertion invalidates iterator, let's use our start mark...
self.insert_and_format_code(buf, text_to_insert, language, self._insert_and_format_code(
marker_widths, start_mark, end_mark, other_tags) buf, text_to_insert, language, marker_widths, start_mark,
end_mark, other_tags)
iterator = buf.get_iter_at_mark(end_mark) iterator = buf.get_iter_at_mark(end_mark)
# The current end of the buffer's contents is the start for the # The current end of the buffer's contents is the start for the
@@ -244,14 +257,13 @@ class ChatSyntaxHighlighter:
# print_special_text method is resetting the plugin_modified variable... # print_special_text method is resetting the plugin_modified variable...
self.textview.plugin_modified = True self.textview.plugin_modified = True
def insert_and_format_code(self, buf, insert_text, language, marker, def _insert_and_format_code(self, buf, insert_text, language, marker,
start_mark, end_mark, other_tags=None): start_mark, end_mark, other_tags=None):
start_iter = buf.get_iter_at_mark(start_mark) start_iter = buf.get_iter_at_mark(start_mark)
if other_tags: if other_tags:
buf.insert_with_tags_by_name(start_iter, insert_text, buf.insert_with_tags_by_name(start_iter, insert_text, *other_tags)
*other_tags)
else: else:
buf.insert(start_iter, insert_text) buf.insert(start_iter, insert_text)
@@ -264,20 +276,16 @@ class ChatSyntaxHighlighter:
log.debug('full text between tags: %s.', tag_start.get_text(tag_end)) 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._format_code(buf, tag_start, s_code, tag_end, e_code, language)
self.textview.plugin_modified = True self.textview.plugin_modified = True
# Set general code block format # Set general code block format
tag = Gtk.TextTag.new() tag = Gtk.TextTag.new()
if self.config.is_bgcolor_override_enabled(): bg_color = self._plugin_config['bgcolor']
tag.set_property('background', self.config.get_bgcolor()) if self._plugin_config['bgcolor_override']:
tag.set_property('paragraph-background', self.config.get_bgcolor()) tag.set_property('background', bg_color)
tag.set_property('font', self.config.get_font()) tag.set_property('paragraph-background', bg_color)
tag.set_property('font', self._plugin_config['font'])
buf.get_tag_table().add(tag) buf.get_tag_table().add(tag)
buf.apply_tag(tag, tag_start, tag_end) buf.apply_tag(tag, tag_start, tag_end)
def __init__(self, config, textview):
self.last_end_mark = None
self.config = config
self.textview = textview

View File

@@ -0,0 +1,206 @@
import logging
import re
import math
from pathlib import Path
import pygments
from gi.repository import Gtk
from gi.repository import Gdk
from gi.repository.Pango import FontDescription
from gi.repository.Pango import Style
from gi.repository.Pango import SCALE
from gajim.common import app
from gajim.plugins.plugins_i18n import _
from gajim.plugins.helpers import get_builder
from syntax_highlight.gtkformatter import GTKFormatter
from syntax_highlight.types import LineBreakOptions
from syntax_highlight.types import CodeMarkerOptions
from syntax_highlight.types import PLUGIN_INTERNAL_NONE_LEXER_ID
log = logging.getLogger('gajim.p.syntax_highlight')
PLUGIN_INTERNAL_NONE_LEXER = ('None (monospace only)',
PLUGIN_INTERNAL_NONE_LEXER_ID)
class SyntaxHighlighterPluginConfig(Gtk.ApplicationWindow):
def __init__(self, plugin, transient):
Gtk.ApplicationWindow.__init__(self)
self.set_application(app.app)
self.set_show_menubar(False)
self.set_title(_('Syntax Highlighter Configuration'))
self.set_transient_for(transient)
self.set_default_size(400, 500)
self.set_type_hint(Gdk.WindowTypeHint.DIALOG)
self.set_modal(True)
self.set_destroy_with_parent(True)
ui_path = Path(__file__).parent
self._ui = get_builder(ui_path.resolve() / 'config_dialog.ui')
self.add(self._ui.main_box)
self.show_all()
self._ui.preview_textview.get_buffer().connect(
'insert-text', self._on_preview_text_inserted)
self._ui.connect_signals(self)
self._lexer_liststore = Gtk.ListStore(str)
self._ui.default_lexer_combobox.set_model(self._lexer_liststore)
self._style_liststore = Gtk.ListStore(str)
self._ui.style_combobox.set_model(self._style_liststore)
self._plugin = plugin
self._lexers = plugin.highlighter_config.get_lexer_list()
self._styles = plugin.highlighter_config.get_styles_list()
self._provider = None
self._add_css_provider()
self._initialize()
def _initialize(self):
default_lexer = self._plugin.highlighter_config.get_default_lexer_name()
for i, lexer in enumerate(self._lexers):
self._lexer_liststore.append([lexer[0]])
if lexer[1] == default_lexer:
self._ui.default_lexer_combobox.set_active(i)
for i, style in enumerate(self._styles):
self._style_liststore.append([style])
if style == self._plugin.config['style']:
self._ui.style_combobox.set_active(i)
self._ui.line_break_combobox.set_active(
self._plugin.config['line_break'].value)
self._ui.code_marker_combobox.set_active(
self._plugin.config['code_marker'])
self._ui.font_button.set_font(self._plugin.config['font'])
bg_override_enabled = self._plugin.config['bgcolor_override']
self._ui.bg_color_checkbutton.set_active(bg_override_enabled)
self._ui.bg_color_colorbutton.set_sensitive(bg_override_enabled)
color = Gdk.RGBA()
if color.parse(self._plugin.config['bgcolor']):
self._ui.bg_color_colorbutton.set_rgba(color)
self._update_preview()
def _lexer_changed(self, widget):
self._plugin.highlighter_config.set_default_lexer(
self._lexers[widget.get_active()][1])
self._update_preview()
def _line_break_changed(self, widget):
self._plugin.config['line_break'] = LineBreakOptions(
widget.get_active())
self._update_preview()
def _code_marker_changed(self, widget):
self._plugin.config['code_marker'] = CodeMarkerOptions(
widget.get_active())
def _bg_color_enabled(self, widget):
override_color = widget.get_active()
self._plugin.config['bgcolor_override'] = override_color
self._ui.bg_color_colorbutton.set_sensitive(override_color)
self._update_preview()
def _bg_color_changed(self, widget):
color = widget.get_rgba()
self._plugin.config['bgcolor'] = color.to_string()
self._update_preview()
def _style_changed(self, widget):
style = self._styles[widget.get_active()]
if style is not None and style != '':
self._plugin.config['style'] = style
self._update_preview()
def _font_changed(self, widget):
font = widget.get_font()
if font is not None and font != '':
self._plugin.config['font'] = font
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 _add_css_provider(self):
self._context = self._ui.preview_textview.get_style_context()
self._provider = Gtk.CssProvider()
self._context.add_provider(
self._provider, Gtk.STYLE_PROVIDER_PRIORITY_USER)
self._context.add_class('syntax-preview')
def _format_preview_text(self):
buf = self._ui.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._plugin.config['style'], start_mark=start_mark)
code = start_iter.get_text(buf.get_end_iter())
lexer = self._plugin.highlighter_config.get_default_lexer()
if lexer != PLUGIN_INTERNAL_NONE_LEXER_ID:
tokens = pygments.lex(code, lexer)
pygments.format(tokens, formatter, buf)
buf.delete_mark(start_mark)
css = self._get_css()
self._provider.load_from_data(bytes(css.encode()))
def _get_css(self):
# Build CSS from Pango.FontDescription
description = FontDescription.from_string(self._plugin.config['font'])
size = description.get_size() / SCALE
style = self._get_string_from_pango_style(description.get_style())
weight = self._pango_to_css_weight(int(description.get_weight()))
family = description.get_family()
font = '%spt %s' % (size, family)
if self._plugin.config['bgcolor_override']:
color = self._plugin.config['bgcolor']
else:
color = '@theme_base_color'
css = '''
.syntax-preview {
font: %s;
font-weight: %s;
font-style: %s;
}
.syntax-preview > text {
background-color: %s;
}
''' % (font, weight, style, color)
return css
@staticmethod
def _pango_to_css_weight(number):
# Pango allows for weight values between 100 and 1000
# CSS allows only full hundred numbers like 100, 200 ..
number = int(number)
if number < 100:
return 100
if number > 900:
return 900
return int(math.ceil(number / 100.0)) * 100
@staticmethod
def _get_string_from_pango_style(style: Style) -> str:
if style == Style.NORMAL:
return 'normal'
if style == Style.ITALIC:
return 'italic'
# Style.OBLIQUE:
return 'oblique'

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.1 --> <!-- Generated with glade 3.36.0 -->
<interface> <interface>
<requires lib="gtk+" version="3.20"/> <requires lib="gtk+" version="3.22"/>
<object class="GtkTextBuffer"/> <object class="GtkTextBuffer"/>
<object class="GtkListStore" id="code_marker_selection"> <object class="GtkListStore" id="code_marker_selection">
<columns> <columns>
@@ -309,7 +309,4 @@
</packing> </packing>
</child> </child>
</object> </object>
<object class="GtkTextBuffer" id="textbuffer1">
<property name="text">Plug-in decription should be displayed here. This text will be erased during PluginsWindow initialization.</property>
</object>
</interface> </interface>

View File

@@ -67,7 +67,7 @@ class GTKFormatter(Formatter):
def format(self, tokensource, outfile): def format(self, tokensource, outfile):
if not isinstance(outfile, Gtk.TextBuffer) or outfile is None: if not isinstance(outfile, Gtk.TextBuffer) or outfile is None:
log.warn("Did not get a buffer to format...") log.warning('Did not get a buffer to format...')
return return
buf = outfile buf = outfile

View File

@@ -0,0 +1,97 @@
import logging
from pygments.lexers import get_lexer_by_name
from pygments.lexers import get_all_lexers
from pygments.styles import get_all_styles
from pygments.util import ClassNotFound
from syntax_highlight.types import PLUGIN_INTERNAL_NONE_LEXER_ID
log = logging.getLogger('gajim.p.syntax_highlight')
PLUGIN_INTERNAL_NONE_LEXER = ('None (monospace only)',
PLUGIN_INTERNAL_NONE_LEXER_ID)
class HighlighterConfig:
def __init__(self, plugin_config):
self._plugin_config = plugin_config
self._lexer_list = self._create_lexer_list()
self._style_list = []
for style in get_all_styles():
self._style_list.append(style)
self._style_list.sort()
self._default_lexer = None
self.set_default_lexer(self._plugin_config['default_lexer'])
@staticmethod
def _create_lexer_list():
# The list we create here contains the plain text name and the lexer's
# id string
lexers = []
# Iteration over get_all_lexers() seems to be broken somehow
# Workaround
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()
# Insert our internal 'none' type at top of the list
lexers.insert(0, PLUGIN_INTERNAL_NONE_LEXER)
return lexers
@staticmethod
def get_lexer_by_name(name):
lexer = None
try:
lexer = get_lexer_by_name(name)
except ClassNotFound:
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_default_lexer(self, name):
if name != PLUGIN_INTERNAL_NONE_LEXER_ID:
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 lexer '
'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)
else:
self._default_lexer = PLUGIN_INTERNAL_NONE_LEXER
self._plugin_config['default_lexer'] = name
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_styles_list(self):
return self._style_list

View File

@@ -1,168 +0,0 @@
import logging
from gi.repository import Gdk
from pygments.lexers import get_lexer_by_name
from pygments.lexers import get_all_lexers
from pygments.styles import get_all_styles
from pygments.util import ClassNotFound
from syntax_highlight.types import LineBreakOptions
from syntax_highlight.types import CodeMarkerOptions
from syntax_highlight.types import PLUGIN_INTERNAL_NONE_LEXER_ID
log = logging.getLogger('gajim.p.syntax_highlight')
class SyntaxHighlighterConfig:
PLUGIN_INTERNAL_NONE_LEXER = ('None (monospace only)',
PLUGIN_INTERNAL_NONE_LEXER_ID)
def _create_lexer_list(self):
# The list we create here contains the plain text name and the lexer's
# id string
lexers = []
# Iteration over get_all_lexers() seems to be broken somehow
# Workaround
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()
# Insert our internal 'none' type at top of the list
lexers.insert(0, self.PLUGIN_INTERNAL_NONE_LEXER)
return lexers
def is_internal_none_lexer(self, lexer):
return lexer == PLUGIN_INTERNAL_NONE_LEXER_ID
def get_internal_none_lexer(self):
return self.PLUGIN_INTERNAL_NONE_LEXER
def get_lexer_by_name(self, name):
lexer = None
try:
lexer = get_lexer_by_name(name)
except ClassNotFound:
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):
if not self.is_internal_none_lexer(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 lexer '
'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)
else:
self.default_lexer = self.PLUGIN_INTERNAL_NONE_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.RGBA):
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

View File

@@ -1,151 +0,0 @@
import re
import pygments
from gi.repository import Gtk
from gi.repository import Gdk
from gi.repository.Pango import FontDescription
from gajim.plugins.gui import GajimPluginConfigDialog
from gajim.plugins.helpers import get_builder
from syntax_highlight.gtkformatter import GTKFormatter
from syntax_highlight.types import LineBreakOptions
from syntax_highlight.types import CodeMarkerOptions
class SyntaxHighlighterPluginConfiguration(GajimPluginConfigDialog):
def init(self):
path = self.plugin.local_file_path('config_dialog.ui')
self._ui = get_builder(path)
box = self.get_content_area()
box.pack_start(self._ui.main_box, True, True, 0)
self._ui.set_translation_domain('gajim_plugins')
self.liststore = Gtk.ListStore(str)
self._ui.default_lexer_combobox.set_model(self.liststore)
self.style_liststore = Gtk.ListStore(str)
self._ui.style_combobox.set_model(self.style_liststore)
self._ui.preview_textview.get_buffer().connect(
'insert-text', self._on_preview_text_inserted)
self._ui.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._ui.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._ui.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._ui.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._ui.bg_color_checkbutton.get_active()
if new != self.config.is_bgcolor_override_enabled():
bg_override_enabled = new
self.config.set_bgcolor_override_enabled(bg_override_enabled)
self._ui.bg_color_colorbutton.set_sensitive(bg_override_enabled)
self._update_preview()
def _bg_color_changed(self, _widget):
new = self._ui.bg_color_colorbutton.get_rgba()
if new != self.config.get_bgcolor():
self.config.set_bgcolor(new)
self._update_preview()
def _style_changed(self, _widget):
new = self._ui.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._ui.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._ui.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()
if not self.config.is_internal_none_lexer(lexer):
tokens = pygments.lex(code, lexer)
pygments.format(tokens, formatter, buf)
buf.delete_mark(start_mark)
self._ui.preview_textview.override_font(
FontDescription.from_string(self.config.get_font()))
color = Gdk.RGBA()
if color.parse(self.config.get_bgcolor()):
self._ui.preview_textview.override_background_color(
Gtk.StateFlags.NORMAL, color)
def on_run(self):
self._ui.default_lexer_combobox.set_active(self.default_lexer_id)
self._ui.line_break_combobox.set_active(
self.config.get_line_break_action())
self._ui.code_marker_combobox.set_active(
self.config.get_code_marker_setting())
self._ui.style_combobox.set_active(self.style_id)
self._ui.font_button.set_font(self.config.get_font())
bg_override_enabled = self.config.is_bgcolor_override_enabled()
self._ui.bg_color_checkbutton.set_active(bg_override_enabled)
self._ui.bg_color_colorbutton.set_sensitive(bg_override_enabled)
color = Gdk.RGBA()
if color.parse(self.config.get_bgcolor()):
self._ui.bg_color_colorbutton.set_rgba(color)

View File

@@ -1,111 +1,91 @@
import logging import logging
import sys from functools import partial
from gajim.plugins import GajimPlugin from gajim.plugins import GajimPlugin
from gajim.plugins.plugins_i18n import _
from syntax_highlight.types import LineBreakOptions from syntax_highlight.types import LineBreakOptions
from syntax_highlight.types import CodeMarkerOptions from syntax_highlight.types import CodeMarkerOptions
from syntax_highlight.types import PLUGIN_INTERNAL_NONE_LEXER_ID from syntax_highlight.types import PLUGIN_INTERNAL_NONE_LEXER_ID
if sys.version_info >= (3, 4):
from importlib.util import find_spec as find_module
else:
from importlib import find_loader as find_module
PYGMENTS_MISSING = 'You are missing Python-Pygments.'
log = logging.getLogger('gajim.p.syntax_highlight') log = logging.getLogger('gajim.p.syntax_highlight')
HAS_PYGMENTS = False
def try_loading_pygments(): try:
success = find_module('pygments') is not None from syntax_highlight.chat_syntax_highlighter import ChatSyntaxHighlighter
if success: from syntax_highlight.config_dialog import SyntaxHighlighterPluginConfig
try: from syntax_highlight.highlighter_config import HighlighterConfig
from syntax_highlight.chat_syntax_highlighter import \ HAS_PYGMENTS = True
ChatSyntaxHighlighter except Exception as exception:
from syntax_highlight.plugin_config_dialog import \ log.error('Could not load pygments: %s', exception)
SyntaxHighlighterPluginConfiguration
from syntax_highlight.plugin_config import SyntaxHighlighterConfig
global SyntaxHighlighterPluginConfiguration
global ChatSyntaxHighlighter
global SyntaxHighlighterConfig
success = True
log.debug("pygments loaded.")
except Exception as exception:
log.error("Import Error: %s.", exception)
success = False
return success
class SyntaxHighlighterPlugin(GajimPlugin): class SyntaxHighlighterPlugin(GajimPlugin):
def on_connect_with_chat_control(self, chat_control):
account = chat_control.contact.account.name
jid = chat_control.contact.jid
if account not in self.ccontrol:
self.ccontrol[account] = {}
self.ccontrol[account][jid] = ChatSyntaxHighlighter(
self.conf, chat_control.conv_textview)
def on_disconnect_from_chat_control(self, chat_control):
account = chat_control.contact.account.name
jid = chat_control.contact.jid
del self.ccontrol[account][jid]
def on_print_real_text(self, text_view, real_text, other_tags, graphics,
iterator, additional):
account = text_view.account
for jid in self.ccontrol[account]:
if self.ccontrol[account][jid].textview != text_view:
continue
self.ccontrol[account][jid].process_text(
real_text, other_tags, graphics, iterator, additional)
return
def try_init(self):
"""
Separating this part of the initialization from the init() method
allows repeating this step again, without reloading the plugin,
i.e. restarting Gajim for instance.
Doing so allows resolving the dependency issues without restart :)
"""
pygments_loaded = try_loading_pygments()
if not pygments_loaded:
return False
self.activatable = True
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()
self.config_dialog = SyntaxHighlighterPluginConfiguration(self)
self.config_dialog.set_config(self.conf)
self.gui_extension_points = {
'chat_control_base': (
self.on_connect_with_chat_control,
self.on_disconnect_from_chat_control),
'print_real_text': (self.on_print_real_text, None), }
return True
def init(self): def init(self):
self.ccontrol = {} self.description = _(
'Source code syntax highlighting in the chat window.\n\n'
'Markdown-style syntax is supported, i.e. text inbetween '
'`single backticks` is rendered as inline code.\n'
'```language\n'
'selection is possible in multi-line code snippets inbetween '
'triple-backticks\n'
'Note the newlines in this case…\n'
'```\n\n'
'Changed settings will take effect after re-opening the message '
'tab/window.')
self.config_default_values = { self.config_default_values = {
'default_lexer': (PLUGIN_INTERNAL_NONE_LEXER_ID, ''), 'default_lexer': (PLUGIN_INTERNAL_NONE_LEXER_ID, ''),
'line_break': (LineBreakOptions.MULTILINE, ''), 'line_break': (LineBreakOptions.MULTILINE, ''),
'style': ('default', ''), 'style': ('default', ''),
'font': ('Monospace 10', ''), 'font': ('Monospace 10', ''),
'bgcolor': ('#ccc', ''), 'bgcolor': ('rgb(200, 200, 200)', ''),
'bgcolor_override': (True, ''), 'bgcolor_override': (True, ''),
'code_marker': (CodeMarkerOptions.AS_COMMENT, ''), 'code_marker': (CodeMarkerOptions.AS_COMMENT, ''),
'pygments_path': (None, ''), } }
is_initialized = self.try_init() self.gui_extension_points = {
'chat_control_base': (
self._connect_chat_control,
self._disconnect_chat_control),
'print_real_text': (self._on_print_real_text, None)
}
if not is_initialized: if not HAS_PYGMENTS:
self.activatable = False self.activatable = False
self.available_text = PYGMENTS_MISSING self.available_text = _('You are missing python-pygments.')
self.config_dialog = None self.config_dialog = None
self._migrate_settings()
self._highlighters = {}
self.config_dialog = partial(SyntaxHighlighterPluginConfig, self)
self.highlighter_config = HighlighterConfig(self.config)
def _migrate_settings(self):
line_break = self.config['line_break']
if isinstance(line_break, int):
self.config['line_break'] = LineBreakOptions(line_break)
def _connect_chat_control(self, chat_control):
highlighter = ChatSyntaxHighlighter(
self.config, self.highlighter_config, chat_control.conv_textview)
self._highlighters[chat_control.control_id] = highlighter
def _disconnect_chat_control(self, chat_control):
highlighter = self._highlighters.get(chat_control.control_id)
if highlighter is not None:
del highlighter
self._highlighters.pop(chat_control.control_id, None)
def _on_print_real_text(self, text_view, real_text, other_tags, graphics,
iterator, additional):
for highlighter in self._highlighters.values():
if highlighter.textview != text_view:
continue
highlighter.process_text(
real_text, other_tags, graphics, iterator, additional)
return
def update_highlighters(self):
for highlighter in self._highlighters.values():
highlighter.update_config(self.config)