Skip to content
Snippets Groups Projects
Commit 1b7442fc authored by Florian Münchbach's avatar Florian Münchbach Committed by Florian Münchbach
Browse files

[syntax_highlight] Integrate with master branch

This commit contains all changes starting with the first integration of
the plugin into the gajim_1.1 branch (76dabe2e) until the current plugin
version V3 (42b9aeb8).
The manifest.ini is updated for compatibility with upcoming Gajim
versions.
parent 85648899
No related branches found
No related tags found
1 merge request!117[syntax_highlight] Integrate with plugin master branch
This diff is collapsed.
# Syntax Highlighting Plugin for Gajim
[Gajim](https://gajim.org) Plugin that highlights source code blocks in chatbox.
## 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 seite](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 chatbox.
```
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 languge is specified with the opening tag or the specified language
could not be identified, the default languge 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 performatted text.
Formatting of such text blocks with monospaced 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 pre-formatted text.
Since text in between the back-tick markers is not further formatted by this
plugin, it can be considered "pre-formatted".
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 pannel. 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.plugin_system.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://trac-plugins.gajim.org/wiki/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/).
from .syntax_highlight import SyntaxHighlighterPlugin
import logging
import re
import pygments
from gi.repository import Gtk
from gajim.plugins.helpers import 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')
elif not self.config.is_internal_none_lexer(lexer):
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 = []
#Less strict, allow prefixed whitespaces: for i in re.finditer(r'(?:^|\n)[ |\t]*(```)\S*[ |\t]*(?:\n|$)', text, re.DOTALL):
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):
"""
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
defined in XEP-0393.
The same applies mirrored to the end marker.
"""
return [(i.start(1), i.end(1), i.group(1)) for i in \
re.finditer(r'(?:^|\s|\*|~|_)(`((?!`).+?)`)(?:\s|\*|~|_|$)', 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):
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, end_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, end_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)
tag_start = buf.get_iter_at_mark(start_mark)
tag_end = buf.get_iter_at_mark(end_mark)
s_code = tag_start.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, tag_start, tag_end)
def __init__(self, config, textview):
self.last_end_mark = None
self.config = config
self.textview = textview
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.1 -->
<interface>
<requires lib="gtk+" version="3.0"/>
<object class="GtkTextBuffer"/>
<object class="GtkListStore" id="code_marker_selection">
<columns>
<!-- column-name column1 -->
<column type="gchararray"/>
</columns>
<data>
<row>
<col id="0" translatable="yes">Treat as code comment</col>
</row>
<row>
<col id="0" translatable="yes">Hide code markers</col>
</row>
</data>
</object>
<object class="GtkListStore" id="line_break_selection">
<columns>
<!-- column-name Text -->
<column type="gchararray"/>
</columns>
<data>
<row>
<col id="0" translatable="yes">Never</col>
</row>
<row>
<col id="0" translatable="yes">Always</col>
</row>
<row>
<col id="0" translatable="yes">Only around multi-line code blocks</col>
</row>
</data>
</object>
<object class="GtkTextBuffer" id="preview_textbuffer">
<property name="text" translatable="yes">// Test your highlighting here
# Test your highlighting here
/* Test your highlighting here */
% Test your highlighting here
; Test your highlighting here
&lt;!-- Test your highlighting here --&gt;</property>
</object>
<object class="GtkWindow" id="window1">
<property name="can_focus">False</property>
<child>
<placeholder/>
</child>
<child>
<object class="GtkBox" id="mainBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkGrid">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_bottom">40</property>
<child>
<object class="GtkLabel" id="label1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Default Language for Syntax Highlighting</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="label2">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Insert Line breaks around Code Blocks</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkComboBox" id="default_lexer_combobox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<signal name="changed" handler="lexer_changed" swapped="no"/>
<child>
<object class="GtkCellRendererText" id="cellrenderertext1"/>
<attributes>
<attribute name="text">0</attribute>
</attributes>
</child>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkComboBox" id="line_break_combobox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="model">line_break_selection</property>
<signal name="changed" handler="line_break_changed" swapped="no"/>
<child>
<object class="GtkCellRendererText" id="cellrenderertext2"/>
<attributes>
<attribute name="text">0</attribute>
</attributes>
</child>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkComboBox" id="style_combobox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<signal name="changed" handler="style_changed" swapped="no"/>
<child>
<object class="GtkCellRendererText" id="cellrenderertext3"/>
<attributes>
<attribute name="text">0</attribute>
</attributes>
</child>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">2</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="label3">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Select Syntax Highlighting Style</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">2</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Select code marker (the backticks) formatting:</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">3</property>
</packing>
</child>
<child>
<object class="GtkComboBox" id="code_marker_combobox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="model">code_marker_selection</property>
<signal name="changed" handler="code_marker_changed" swapped="no"/>
<child>
<object class="GtkCellRendererText" id="cellrenderertext4"/>
<attributes>
<attribute name="text">0</attribute>
</attributes>
</child>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">3</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Select Font for Code Snippets:</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">4</property>
</packing>
</child>
<child>
<object class="GtkFontButton" id="font_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="font">Sans 12</property>
<property name="language">de-de</property>
<property name="preview_text"/>
<property name="use_font">True</property>
<signal name="font-set" handler="font_changed" swapped="no"/>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">4</property>
</packing>
</child>
<child>
<object class="GtkCheckButton" id="bg_color_checkbutton">
<property name="label" translatable="yes">Set Background Color</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="halign">start</property>
<property name="draw_indicator">True</property>
<signal name="toggled" handler="bg_color_enabled" swapped="no"/>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">5</property>
</packing>
</child>
<child>
<object class="GtkColorButton" id="bg_color_colorbutton">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="title" translatable="yes">Chose the background color for code blocks</property>
<property name="show_editor">True</property>
<signal name="color-set" handler="bg_color_changed" swapped="no"/>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">5</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkSeparator">
<property name="visible">True</property>
<property name="can_focus">False</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_top">20</property>
<property name="margin_bottom">5</property>
<property name="label" translatable="yes">Test how code blocks will look like in the chat window:</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkTextView" id="preview_textview">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="wrap_mode">word-char</property>
<property name="buffer">preview_textbuffer</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
<child>
<object class="GtkSeparator">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_top">20</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">4</property>
</packing>
</child>
<child>
<placeholder/>
</child>
</object>
</child>
</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>
import logging
from gi.repository import Gtk as gtk
from gi.repository import Pango
from pygments.formatter import Formatter
from gajim.plugins.helpers import log
log = logging.getLogger('gajim.plugin_system.syntax_highlight')
class GTKFormatter(Formatter):
name = 'GTK Formatter'
aliases = ['textbuffer', 'gtk']
filenames = []
def __init__(self, **options):
super(GTKFormatter, self).__init__(**options)
#Formatter.__init__(self, **options)
self.tags = {}
self.mark = options.get('start_mark', None)
@staticmethod
def create_tag_for_token(ttype, highlighting_style):
style = highlighting_style.style_for_token(ttype)
tag = gtk.TextTag.new()
if 'bgcolor' in style and not style['bgcolor'] is None:
tag.set_property('background', '#%s' % style['bgcolor'])
if 'bold' in style and style['bold']:
tag.set_property('weight', Pango.Weight.BOLD)
if 'border' in style and not style['border'] is None:
pass
if 'color' in style and not style['color'] is None:
tag.set_property('foreground', '#%s' % style['color'])
if 'italic' in style and style['italic']:
tag.set_property('style', Pango.Style.ITALIC)
if 'mono' in style and not style['mono'] is None:
tag.set_property('family', 'Monospace')
tag.set_property('family-set', True)
if 'roman' in style and not style['roman'] is None:
pass
if 'sans' in style and not style['sans'] is None:
tag.set_property('family', 'Sans')
tag.set_property('family-set', True)
if 'underline' in style and style['underline']:
tag.set_property('underline', 'single')
return tag
def get_tag(self, ttype, buf):
"""
Creates, stores and returs a tag for a given token type.
This method ensures that a tag style is created only once.
Furthermore, the tag will be added to the given Gtk.TextBuffer's tag table.
"""
tag = None
if ttype in self.tags:
tag = self.tags[ttype]
else:
tag = GTKFormatter.create_tag_for_token(ttype, self.style)
self.tags[ttype] = tag
buf.get_tag_table().add(tag)
return tag
def set_insert_pos_mark(self, mark):
self.mark = mark
def format(self, tokensource, outfile):
if not isinstance(outfile, gtk.TextBuffer) or outfile is None:
log.warn("Did not get a buffer to format...")
return
buf = outfile
end_iter = buf.get_end_iter()
start_mark = self.mark
start_iter = buf.get_iter_at_mark(start_mark) if not start_mark is None \
else end_iter
last_ttype = None
last_start = start_iter
last_end = buf.get_end_iter()
last_fixed_start = last_start
reset = True
for ttype, value in tokensource:
search = None
if last_ttype is not None and ttype != last_ttype:
tag = self.get_tag(last_ttype, buf)
buf.apply_tag(tag, last_fixed_start, last_end)
search = last_end.forward_search(value, gtk.TextSearchFlags.TEXT_ONLY, end_iter)
reset = True
else:
# in case last_ttype is None, i.e. first loop walkthrough:
# last_start to end_iter is the full code block.
search = last_start.forward_search(value, gtk.TextSearchFlags.TEXT_ONLY, end_iter)
# Prepare for next iteration
last_ttype = ttype
if search is not None:
(last_start, last_end) = search
# If we've found the end of a sequence of similar type tokens or if
# we are in the first loop iteration, set the fixed point
if reset:
last_fixed_start = last_start
reset = False
else:
# Hm... Nothing found, but tags left? Seams there's nothing we
# can do now.
break
[info]
# Don't forget to point to the correct README.md file when switching to another
# branch or so...
name: Source Code Syntax Highlight
short_name: syntax_highlight
version: 1.2.3
description: Source Code Syntax Highlighting in the chatbox.
It uses markdown-style syntax, i.e. text in between `single backticks` is
rendered as inline code, ```language
selection is possible in multi-line code snippets in between triple-backticks
Note the newlines in this case...
```
*Note:* You need to have pygments installed.
Please refer to the plugin's Wiki page for more information.
Changing settings will take effect after closing and opening the message
tab/window again.
authors = Florian Muenchbach
homepage = https://dev.gajim.org/gajim/gajim-plugins/blob/master/syntax_highlight/README.md
min_gajim_version: 1.1.91
max_gajim_version: 1.2.90
from gajim.plugins.helpers import log
from gi.repository import Gdk
from pygments.lexers import get_lexer_by_name, get_all_lexers
from pygments.styles import get_all_styles
from .types import LineBreakOptions, CodeMarkerOptions, \
PLUGIN_INTERNAL_NONE_LEXER_ID
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. 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()
# 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:
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 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.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
import re
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
from .gtkformatter import GTKFormatter
from .types import LineBreakOptions, CodeMarkerOptions
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()
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.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)
syntax_highlight/syntax_highlight.png

465 B

import logging
import sys
if sys.version_info >= (3, 4):
from importlib.util import find_spec as find_module
else:
from importlib import find_loader as find_module
from gajim.plugins.helpers import log_calls, log
from gajim.plugins import GajimPlugin
from .types import MatchType, LineBreakOptions, CodeMarkerOptions, \
PLUGIN_INTERNAL_NONE_LEXER_ID
log = logging.getLogger('gajim.plugin_system.syntax_highlight')
def try_loading_pygments():
success = find_module('pygments') is not None
if success:
try:
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:
log.error("Import Error: %s.", exception)
success = False
return success
PYGMENTS_MISSING = 'You are missing Python-Pygments.'
class SyntaxHighlighterPlugin(GajimPlugin):
@log_calls('SyntaxHighlighterPlugin')
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)
@log_calls('SyntaxHighlighterPlugin')
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]
@log_calls('SyntaxHighlighterPlugin')
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
@log_calls('SyntaxHighlighterPlugin')
def init(self):
self.ccontrol = {}
self.config_default_values = {
'default_lexer' : (PLUGIN_INTERNAL_NONE_LEXER_ID, ''),
'line_break' : (LineBreakOptions.MULTILINE, ''),
'style' : ('default', ''),
'font' : ('Monospace 10', ''),
'bgcolor' : ('#ccc', ''),
'bgcolor_override' : (True, ''),
'code_marker' : (CodeMarkerOptions.AS_COMMENT, ''),
'pygments_path' : (None, ''),
}
is_initialized = self.try_init()
if not is_initialized:
self.activatable = False
self.available_text = PYGMENTS_MISSING
self.config_dialog = None
from enum import Enum, IntEnum, unique
PLUGIN_INTERNAL_NONE_LEXER_ID='_syntax_highlight_internal_none_type'
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
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment