Philipp Hörist pushed to branch master at gajim / gajim
Commits:
4 changed files:
- gajim/common/regex.py
- gajim/gtk/control.py
- gajim/gtk/conversation/scrolled.py
- test/no_gui/test_regex.py
Changes:
| 1 | -import re
|
|
| 2 | - |
|
| 3 | - |
|
| 4 | -def _get_link_pattern():
|
|
| 5 | - # regexp meta characters are: . ^ $ * + ? { } [ ] \ | ( )
|
|
| 6 | - # one escapes the metachars with \
|
|
| 7 | - # \S matches anything but ' ' '\t' '\n' '\r' '\f' and '\v'
|
|
| 8 | - # \s matches any whitespace character
|
|
| 9 | - # \w any alphanumeric character
|
|
| 10 | - # \W any non-alphanumeric character
|
|
| 11 | - # \b means word boundary. This is a zero-width assertion that
|
|
| 12 | - # matches only at the beginning or end of a word.
|
|
| 13 | - # ^ matches at the beginning of lines
|
|
| 14 | - #
|
|
| 15 | - # * means 0 or more times
|
|
| 16 | - # + means 1 or more times
|
|
| 17 | - # ? means 0 or 1 time
|
|
| 18 | - # | means or
|
|
| 19 | - # [^*] anything but '*' (inside [] you don't have to escape metachars)
|
|
| 20 | - # [^\s*] anything but whitespaces and '*'
|
|
| 21 | - # (?<!\S) is a one char lookbehind assertion and asks for any leading
|
|
| 22 | - # whitespace
|
|
| 23 | - # and matches beginning of lines so we have correct formatting detection
|
|
| 24 | - # even if the text is just '*foo*'
|
|
| 25 | - # (?!\S) is the same thing but it's a lookahead assertion
|
|
| 26 | - # \S*[^\s\W] --> in the matching string don't match ? or ) etc.. if at
|
|
| 27 | - # the end
|
|
| 28 | - # so http://be) will match http://be and http://be)be) will match
|
|
| 29 | - # http://be)be
|
|
| 30 | - |
|
| 31 | - legacy_prefixes = r'((?<=\()(www|ftp)\.([A-Za-z0-9\.\-_~:/\?#\[\]@!\$'\
|
|
| 32 | - r"&'\(\)\*\+,;=]|%[A-Fa-f0-9]{2})+(?=\)))"\
|
|
| 33 | - r"|((www|ftp)\.([A-Za-z0-9\.\-_~:/\?#\[\]@!\$&'\(\)\*\+,;=]"\
|
|
| 34 | - r'|%[A-Fa-f0-9]{2})+'\
|
|
| 35 | - r"\.([A-Za-z0-9\.\-_~:/\?#\[\]@!\$&'\(\)\*\+,;=]|%[A-Fa-f0-9]{2})+)"
|
|
| 36 | - # NOTE: it's ok to catch www.gr such stuff exist!
|
|
| 37 | - |
|
| 38 | - # FIXME: recognize xmpp: and treat it specially
|
|
| 39 | - links = r'((?<=\()[A-Za-z][A-Za-z0-9\+\.\-]*:'\
|
|
| 40 | - r"([\w\.\-_~:/\?#\[\]@!\$&'\(\)\*\+,;=]|%[A-Fa-f0-9]{2})+"\
|
|
| 41 | - r'(?=\)))|(\w[\w\+\.\-]*:([^<>\s]|%[A-Fa-f0-9]{2})+)'
|
|
| 42 | - |
|
| 43 | - # 2nd one: at_least_one_char@at_least_one_char.at_least_one_char
|
|
| 44 | - mail = r'\bmailto:\S*[^\s\W]|' r'\b\S+@\S+\.\S*[^\s\W]'
|
|
| 45 | - |
|
| 46 | - link_pattern = links + '|' + mail + '|' + legacy_prefixes
|
|
| 47 | - return link_pattern
|
|
| 48 | - |
|
| 49 | - |
|
| 50 | -def _get_basic_pattern():
|
|
| 51 | - basic_pattern = _get_link_pattern()
|
|
| 52 | - # detects eg. *b* *bold* *bold bold* test *bold* *bold*! (*bold*)
|
|
| 53 | - # doesn't detect (it's a feature :P) * bold* *bold * * bold * test*bold*
|
|
| 54 | - formatting = r'|(?<!\w)' r'\*[^\s*]' r'([^*]*[^\s*])?' r'\*(?!\w)|'\
|
|
| 55 | - r'(?<!\S)' r'~[^\s~]' r'([^~]*[^\s~])?' r'~(?!\S)|'\
|
|
| 56 | - r'(?<!\w)' r'_[^\s_]' r'([^_]*[^\s_])?' r'_(?!\w)'
|
|
| 57 | - return basic_pattern + formatting
|
|
| 1 | +# This file is part of Gajim.
|
|
| 2 | +#
|
|
| 3 | +# Gajim is free software; you can redistribute it and/or modify
|
|
| 4 | +# it under the terms of the GNU General Public License as published
|
|
| 5 | +# by the Free Software Foundation; version 3 only.
|
|
| 6 | +#
|
|
| 7 | +# Gajim is distributed in the hope that it will be useful,
|
|
| 8 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
| 9 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
| 10 | +# GNU General Public License for more details.
|
|
| 11 | +#
|
|
| 12 | +# You should have received a copy of the GNU General Public License
|
|
| 13 | +# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
|
|
| 58 | 14 | |
| 15 | +import re
|
|
| 59 | 16 | |
| 60 | -def _get_emot_and_basic_pattern(use_ascii_formatting: bool = True) -> str:
|
|
| 61 | - from gajim.gui.emoji_data import emoji_data
|
|
| 62 | - # because emoticons match later (in the string) they need to be after
|
|
| 63 | - # basic matches that may occur earlier
|
|
| 64 | - emoticons = emoji_data.get_regex()
|
|
| 65 | - |
|
| 66 | - if use_ascii_formatting:
|
|
| 67 | - pattern = _get_basic_pattern()
|
|
| 68 | - else:
|
|
| 69 | - pattern = _get_link_pattern()
|
|
| 70 | - |
|
| 71 | - return '%s|%s' % (pattern, emoticons)
|
|
| 72 | - |
|
| 73 | - |
|
| 74 | -LINK_REGEX = re.compile(_get_link_pattern(), re.I | re.U)
|
|
| 75 | - |
|
| 76 | -# link pattern + ASCII formatting
|
|
| 77 | -BASIC_REGEX = re.compile(_get_basic_pattern(), re.IGNORECASE)
|
|
| 78 | - |
|
| 79 | -# emoticons + link pattern
|
|
| 80 | -EMOT_AND_LINK_REGEX = re.compile(_get_emot_and_basic_pattern(False),
|
|
| 81 | - re.IGNORECASE)
|
|
| 82 | - |
|
| 83 | -# emoticons + link pattern + ASCII formatting
|
|
| 84 | -EMOT_AND_BASIC_REGEX = re.compile(_get_emot_and_basic_pattern(True),
|
|
| 85 | - re.IGNORECASE)
|
|
| 86 | 17 | |
| 87 | 18 | INVALID_XML_CHARS_REGEX = re.compile(
|
| 88 | 19 | '[\x00-\x08]|[\x0b-\x0c]|[\x0e-\x1f]|[\ud800-\udfff]|[\ufffe-\uffff]')
|
| ... | ... | @@ -502,7 +502,7 @@ def scroll_to_message(self, log_line_id: int, timestamp: float) -> None: |
| 502 | 502 | row = self.conversation_view.get_row_by_log_line_id(log_line_id)
|
| 503 | 503 | if row is None:
|
| 504 | 504 | # Clear view and reload conversation around timestamp
|
| 505 | - self._scrolled_view.block_request_signal(True)
|
|
| 505 | + self._scrolled_view.block_signals(True)
|
|
| 506 | 506 | self.reset_view()
|
| 507 | 507 | before, at_after = app.storage.archive.get_conversation_around(
|
| 508 | 508 | self.contact.account, self.contact.jid, timestamp)
|
| ... | ... | @@ -512,7 +512,7 @@ def scroll_to_message(self, log_line_id: int, timestamp: float) -> None: |
| 512 | 512 | GLib.idle_add(
|
| 513 | 513 | self.conversation_view.scroll_to_message_and_highlight,
|
| 514 | 514 | log_line_id)
|
| 515 | - GLib.idle_add(self._scrolled_view.block_request_signal, False)
|
|
| 515 | + GLib.idle_add(self._scrolled_view.block_signals, False)
|
|
| 516 | 516 | |
| 517 | 517 | def _fetch_n_lines_history(self,
|
| 518 | 518 | _scrolled: Gtk.ScrolledWindow,
|
| ... | ... | @@ -520,7 +520,7 @@ def _fetch_n_lines_history(self, |
| 520 | 520 | n_lines: int
|
| 521 | 521 | ) -> None:
|
| 522 | 522 | |
| 523 | - self._scrolled_view.block_request_signal(True)
|
|
| 523 | + self._scrolled_view.block_signals(True)
|
|
| 524 | 524 | |
| 525 | 525 | if before:
|
| 526 | 526 | row = self.conversation_view.get_first_message_row()
|
| ... | ... | @@ -541,7 +541,7 @@ def _fetch_n_lines_history(self, |
| 541 | 541 | |
| 542 | 542 | if not messages:
|
| 543 | 543 | self._scrolled_view.set_history_complete(before, True)
|
| 544 | - self._scrolled_view.block_request_signal(False)
|
|
| 544 | + self._scrolled_view.block_signals(False)
|
|
| 545 | 545 | return
|
| 546 | 546 | |
| 547 | 547 | self.add_messages(messages)
|
| ... | ... | @@ -553,7 +553,7 @@ def _fetch_n_lines_history(self, |
| 553 | 553 | # if self.conversation_view.reduce_message_count(before):
|
| 554 | 554 | # self._scrolled_view.set_history_complete(before, False)
|
| 555 | 555 | |
| 556 | - self._scrolled_view.block_request_signal(False)
|
|
| 556 | + self._scrolled_view.block_signals(False)
|
|
| 557 | 557 | |
| 558 | 558 | def add_messages(self, messages: list[ConversationRow]):
|
| 559 | 559 | for msg in messages:
|
| ... | ... | @@ -14,6 +14,7 @@ |
| 14 | 14 | |
| 15 | 15 | from __future__ import annotations
|
| 16 | 16 | |
| 17 | +from typing import Any
|
|
| 17 | 18 | from typing import Optional
|
| 18 | 19 | |
| 19 | 20 | from gi.repository import Gtk
|
| ... | ... | @@ -66,7 +67,7 @@ def __init__(self) -> None: |
| 66 | 67 | self._upper_complete: bool = False
|
| 67 | 68 | self._lower_complete: bool = False
|
| 68 | 69 | self._requesting: Optional[str] = None
|
| 69 | - self._block_request_signal = False
|
|
| 70 | + self._block_signals = False
|
|
| 70 | 71 | |
| 71 | 72 | vadjustment = self.get_vadjustment()
|
| 72 | 73 | vadjustment.connect('notify::upper', self._on_adj_upper_changed)
|
| ... | ... | @@ -77,14 +78,16 @@ def __init__(self) -> None: |
| 77 | 78 | self.set_focus_vadjustment(Gtk.Adjustment())
|
| 78 | 79 | |
| 79 | 80 | def clear(self) -> None:
|
| 80 | - self._block_request_signal = True
|
|
| 81 | + self._block_signals = True
|
|
| 82 | + self._reset()
|
|
| 81 | 83 | self._view.clear()
|
| 82 | 84 | |
| 83 | 85 | def switch_contact(self, contact: ChatContactT) -> None:
|
| 84 | - self.reset()
|
|
| 86 | + self._block_signals = True
|
|
| 87 | + self._reset()
|
|
| 85 | 88 | self._view.switch_contact(contact)
|
| 86 | - self._block_request_signal = False
|
|
| 87 | - self.emit('request-history', True)
|
|
| 89 | + self._block_signals = False
|
|
| 90 | + self._emit('request-history', True)
|
|
| 88 | 91 | |
| 89 | 92 | def get_autoscroll(self) -> bool:
|
| 90 | 93 | return self._autoscroll
|
| ... | ... | @@ -92,18 +95,25 @@ def get_autoscroll(self) -> bool: |
| 92 | 95 | def get_view(self) -> ConversationView:
|
| 93 | 96 | return self._view
|
| 94 | 97 | |
| 95 | - def block_request_signal(self, value: bool) -> None:
|
|
| 96 | - self._block_request_signal = value
|
|
| 98 | + def block_signals(self, value: bool) -> None:
|
|
| 99 | + self._block_signals = value
|
|
| 97 | 100 | |
| 98 | - def reset(self) -> None:
|
|
| 101 | + def _emit(self, signal_name: str, *args: Any) -> None:
|
|
| 102 | + if not self._block_signals:
|
|
| 103 | + self.emit(signal_name, *args)
|
|
| 104 | + |
|
| 105 | + def _reset(self) -> None:
|
|
| 99 | 106 | self._current_upper = 0
|
| 100 | 107 | self._request_history_at_upper = None
|
| 101 | 108 | self._upper_complete = False
|
| 102 | 109 | self._lower_complete = False
|
| 103 | 110 | self._requesting = None
|
| 104 | - self._view.reset()
|
|
| 105 | 111 | self.set_history_complete(True, False)
|
| 106 | 112 | |
| 113 | + def reset(self) -> None:
|
|
| 114 | + self._reset()
|
|
| 115 | + self._view.reset()
|
|
| 116 | + |
|
| 107 | 117 | def set_history_complete(self, before: bool, complete: bool) -> None:
|
| 108 | 118 | if before:
|
| 109 | 119 | self._upper_complete = complete
|
| ... | ... | @@ -134,11 +144,11 @@ def _on_adj_upper_changed(self, |
| 134 | 144 | |
| 135 | 145 | if upper == adj.get_page_size():
|
| 136 | 146 | # There is no scrollbar
|
| 137 | - if not self._block_request_signal:
|
|
| 138 | - self.emit('request-history', True)
|
|
| 147 | + if not self._block_signals:
|
|
| 148 | + self._emit('request-history', True)
|
|
| 139 | 149 | self._lower_complete = True
|
| 140 | 150 | self._autoscroll = True
|
| 141 | - self.emit('autoscroll-changed', self._autoscroll)
|
|
| 151 | + self._emit('autoscroll-changed', self._autoscroll)
|
|
| 142 | 152 | |
| 143 | 153 | self._requesting = None
|
| 144 | 154 | |
| ... | ... | @@ -152,10 +162,10 @@ def _on_adj_value_changed(self, |
| 152 | 162 | bottom = adj.get_upper() - adj.get_page_size()
|
| 153 | 163 | if (bottom - adj.get_value()) < 1:
|
| 154 | 164 | self._autoscroll = True
|
| 155 | - self.emit('autoscroll-changed', self._autoscroll)
|
|
| 165 | + self._emit('autoscroll-changed', self._autoscroll)
|
|
| 156 | 166 | else:
|
| 157 | 167 | self._autoscroll = False
|
| 158 | - self.emit('autoscroll-changed', self._autoscroll)
|
|
| 168 | + self._emit('autoscroll-changed', self._autoscroll)
|
|
| 159 | 169 | |
| 160 | 170 | if self._upper_complete:
|
| 161 | 171 | self._request_history_at_upper = None
|
| ... | ... | @@ -178,8 +188,8 @@ def _on_adj_value_changed(self, |
| 178 | 188 | self._request_history_at_upper = adj.get_upper()
|
| 179 | 189 | # Workaround: https://gitlab.gnome.org/GNOME/gtk/merge_requests/395
|
| 180 | 190 | self.set_kinetic_scrolling(False)
|
| 181 | - if not self._block_request_signal:
|
|
| 182 | - self.emit('request-history', True)
|
|
| 191 | + if not self._block_signals:
|
|
| 192 | + self._emit('request-history', True)
|
|
| 183 | 193 | self._requesting = 'before'
|
| 184 | 194 | elif (adj.get_upper() - (adj.get_value() + adj.get_page_size()) <
|
| 185 | 195 | distance):
|
| ... | ... | @@ -188,6 +198,6 @@ def _on_adj_value_changed(self, |
| 188 | 198 | return
|
| 189 | 199 | # Workaround: https://gitlab.gnome.org/GNOME/gtk/merge_requests/395
|
| 190 | 200 | self.set_kinetic_scrolling(False)
|
| 191 | - if not self._block_request_signal:
|
|
| 192 | - self.emit('request-history', False)
|
|
| 201 | + if not self._block_signals:
|
|
| 202 | + self._emit('request-history', False)
|
|
| 193 | 203 | self._requesting = 'after' |
| 1 | +from typing import Any
|
|
| 2 | + |
|
| 1 | 3 | import unittest
|
| 2 | 4 | |
| 3 | -from gajim import gui
|
|
| 4 | -gui.init('gtk')
|
|
| 5 | -import gajim.common.regex as regex
|
|
| 5 | +from gajim.common.styling import URI_RX
|
|
| 6 | +from gajim.common.styling import ADDRESS_RX
|
|
| 6 | 7 | |
| 7 | -class Test(unittest.TestCase):
|
|
| 8 | - def test_links_regexp_entire(self):
|
|
| 9 | - def assert_matches_all(str_):
|
|
| 10 | - m = regex.BASIC_REGEX.match(str_)
|
|
| 11 | 8 | |
| 12 | - # the match should equal the string
|
|
| 13 | - str_span = (0, len(str_))
|
|
| 14 | - self.assertEqual(m.span(), str_span)
|
|
| 9 | +TEST_URIS = [
|
|
| 10 | + 'http://google.com/',
|
|
| 11 | + 'http://google.com',
|
|
| 12 | + 'http://www.google.ca/search?q=xmpp',
|
|
| 13 | + 'http://tools.ietf.org/html/draft-saintandre-rfc3920bis-05#section-12.3',
|
|
| 14 | + 'http://en.wikipedia.org/wiki/Protocol_(computing)',
|
|
| 15 | + 'http://en.wikipedia.org/wiki/Protocol_%28computing%29',
|
|
| 16 | +]
|
|
| 15 | 17 | |
| 16 | - # these entire strings should be parsed as links
|
|
| 17 | - assert_matches_all('http://google.com/')
|
|
| 18 | - assert_matches_all('http://google.com')
|
|
| 19 | - assert_matches_all('http://www.google.ca/search?q=xmpp')
|
|
| 18 | +ADDRESSES = [
|
|
| 19 | + 'mailto:test@example.org',
|
|
| 20 | + 'xmpp:example-node@example.com',
|
|
| 21 | + # 'xmpp:example-node@example.com/some-resource', FIXME
|
|
| 22 | + 'xmpp:example-node@example.com?message',
|
|
| 23 | +]
|
|
| 20 | 24 | |
| 21 | - assert_matches_all('http://tools.ietf.org/html/draft-saintandre-rfc3920bis-05#section-12.3')
|
|
| 22 | 25 | |
| 23 | - assert_matches_all('http://en.wikipedia.org/wiki/Protocol_(computing)')
|
|
| 24 | - assert_matches_all(
|
|
| 25 | - 'http://en.wikipedia.org/wiki/Protocol_%28computing%29')
|
|
| 26 | +class Test(unittest.TestCase):
|
|
| 27 | + def test_links_regexp_entire(self):
|
|
| 28 | + def assert_matches_all(regex: Any, uri_str: str) -> None:
|
|
| 29 | + m = regex.match(uri_str)
|
|
| 30 | + if m is None:
|
|
| 31 | + raise AssertionError('Failed: %s' % uri_str)
|
|
| 32 | + |
|
| 33 | + str_span = (0, len(uri_str))
|
|
| 34 | + self.assertEqual(m.span(), str_span, uri_str)
|
|
| 26 | 35 | |
| 27 | - assert_matches_all('mailto:test@example.org')
|
|
| 36 | + for uri in TEST_URIS:
|
|
| 37 | + assert_matches_all(URI_RX, uri)
|
|
| 28 | 38 | |
| 29 | - assert_matches_all('xmpp:example-node@example.com')
|
|
| 30 | - assert_matches_all('xmpp:example-node@example.com/some-resource')
|
|
| 31 | - assert_matches_all('xmpp:example-node@example.com?message')
|
|
| 32 | - assert_matches_all('xmpp://guest@example.com/support@example.com?message')
|
|
| 39 | + for addr in ADDRESSES:
|
|
| 40 | + assert_matches_all(ADDRESS_RX, addr)
|
|
| 33 | 41 | |
| 34 | 42 | |
| 35 | -if __name__ == "__main__":
|
|
| 43 | +if __name__ == '__main__':
|
|
| 36 | 44 | unittest.main() |