Philipp Hörist pushed to branch master at gajim / gajim
Commits:
-
6690921d
by lovetox at 2021-12-14T22:43:04+01:00
4 changed files:
- gajim/common/styling.py
- gajim/gtk/conversation/plain_widget.py
- gajim/gtk/util.py
- test/no_gui/test_styling.py
Changes:
| 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/>.
|
|
| 14 |
+ |
|
| 15 |
+from __future__ import annotations
|
|
| 16 |
+ |
|
| 17 |
+from typing import Union
|
|
| 18 |
+from typing import Match
|
|
| 19 |
+ |
|
| 1 | 20 |
import string
|
| 2 | 21 |
import re
|
| 3 | 22 |
from dataclasses import dataclass
|
| 4 | 23 |
from dataclasses import field
|
| 5 | 24 |
|
| 25 |
+from gi.repository import GLib
|
|
| 26 |
+ |
|
| 6 | 27 |
PRE = '`'
|
| 7 | 28 |
STRONG = '*'
|
| 8 | 29 |
STRIKE = '~'
|
| 9 | 30 |
EMPH = '_'
|
| 10 | 31 |
|
| 11 |
-QUOTE = '> '
|
|
| 12 |
-PRE_TEXT = '```'
|
|
| 13 |
- |
|
| 14 | 32 |
WHITESPACE = set(string.whitespace)
|
| 15 |
-BLOCK_DIRS = set([QUOTE, PRE_TEXT])
|
|
| 16 | 33 |
SPAN_DIRS = set([PRE, STRONG, STRIKE, EMPH])
|
| 17 | 34 |
VALID_SPAN_START = WHITESPACE | SPAN_DIRS
|
| 18 | 35 |
|
| 19 |
-SD = 0
|
|
| 20 |
-SD_POS = 1
|
|
| 21 |
- |
|
| 22 | 36 |
PRE_RX = r'(?P<pre>^```.+?(^```$(.|\Z)))'
|
| 23 | 37 |
PRE_NESTED_RX = r'(?P<pre>^```.+?((^```$(.|\Z))|\Z))'
|
| 24 | 38 |
QUOTE_RX = r'(?P<quote>^(?=>).*?(^|\Z)(?!>))'
|
| ... | ... | @@ -34,6 +48,10 @@ |
| 34 | 48 |
ADDRESS_RX = re.compile(ADDRESS_RX)
|
| 35 | 49 |
|
| 36 | 50 |
|
| 51 |
+SD = 0
|
|
| 52 |
+SD_POS = 1
|
|
| 53 |
+ |
|
| 54 |
+ |
|
| 37 | 55 |
@dataclass
|
| 38 | 56 |
class StyleObject:
|
| 39 | 57 |
start: int
|
| ... | ... | @@ -41,28 +59,34 @@ class StyleObject: |
| 41 | 59 |
text: str
|
| 42 | 60 |
|
| 43 | 61 |
|
| 44 |
-class URIMarkup:
|
|
| 45 |
- def get_markup_string(self):
|
|
| 46 |
- return f'<a href="{self.text}">{self.text}</a>'
|
|
| 62 |
+@dataclass
|
|
| 63 |
+class BaseUri(StyleObject):
|
|
| 64 |
+ |
|
| 65 |
+ start_byte: int
|
|
| 66 |
+ end_byte: int
|
|
| 67 |
+ |
|
| 68 |
+ def get_markup_string(self) -> str:
|
|
| 69 |
+ text = GLib.markup_escape_text(self.text)
|
|
| 70 |
+ return f'<a href="{text}">{text}</a>'
|
|
| 47 | 71 |
|
| 48 | 72 |
|
| 49 | 73 |
@dataclass
|
| 50 |
-class Uri(StyleObject, URIMarkup):
|
|
| 74 |
+class Uri(BaseUri):
|
|
| 51 | 75 |
name: str = field(default='uri', init=False)
|
| 52 | 76 |
|
| 53 | 77 |
|
| 54 | 78 |
@dataclass
|
| 55 |
-class Address(StyleObject, URIMarkup):
|
|
| 79 |
+class Address(BaseUri):
|
|
| 56 | 80 |
name: str = field(default='address', init=False)
|
| 57 | 81 |
|
| 58 | 82 |
|
| 59 | 83 |
@dataclass
|
| 60 |
-class XMPPAddress(StyleObject, URIMarkup):
|
|
| 84 |
+class XMPPAddress(BaseUri):
|
|
| 61 | 85 |
name: str = field(default='xmppadr', init=False)
|
| 62 | 86 |
|
| 63 | 87 |
|
| 64 | 88 |
@dataclass
|
| 65 |
-class MailAddress(StyleObject, URIMarkup):
|
|
| 89 |
+class MailAddress(BaseUri):
|
|
| 66 | 90 |
name: str = field(default='mailadr', init=False)
|
| 67 | 91 |
|
| 68 | 92 |
|
| ... | ... | @@ -70,17 +94,17 @@ class MailAddress(StyleObject, URIMarkup): |
| 70 | 94 |
class Block(StyleObject):
|
| 71 | 95 |
|
| 72 | 96 |
@classmethod
|
| 73 |
- def from_match(cls, match):
|
|
| 97 |
+ def from_match(cls, match: Match[str]) -> Block:
|
|
| 74 | 98 |
return cls(start=match.start(),
|
| 75 | 99 |
end=match.end(),
|
| 76 |
- text=str(match.group(cls.name)))
|
|
| 100 |
+ text=match.group(cls.name))
|
|
| 77 | 101 |
|
| 78 | 102 |
|
| 79 | 103 |
@dataclass
|
| 80 | 104 |
class PlainBlock(Block):
|
| 81 | 105 |
name: str = field(default='plain', init=False)
|
| 82 |
- spans: list = field(default_factory=list)
|
|
| 83 |
- uris: list = field(default_factory=list)
|
|
| 106 |
+ spans: list[Span] = field(default_factory=list)
|
|
| 107 |
+ uris: list[BaseUri] = field(default_factory=list)
|
|
| 84 | 108 |
|
| 85 | 109 |
|
| 86 | 110 |
@dataclass
|
| ... | ... | @@ -91,34 +115,41 @@ class PreBlock(Block): |
| 91 | 115 |
@dataclass
|
| 92 | 116 |
class QuoteBlock(Block):
|
| 93 | 117 |
name: str = field(default='quote', init=False)
|
| 94 |
- blocks: list = field(default_factory=list)
|
|
| 118 |
+ blocks: list[Block] = field(default_factory=list)
|
|
| 95 | 119 |
|
| 96 |
- def unquote(self):
|
|
| 120 |
+ def unquote(self) -> str:
|
|
| 97 | 121 |
return UNQUOTE_RX.sub('', self.text)
|
| 98 | 122 |
|
| 99 | 123 |
|
| 100 | 124 |
@dataclass
|
| 101 |
-class PlainSpan(StyleObject):
|
|
| 125 |
+class Span(StyleObject):
|
|
| 126 |
+ start_byte: int
|
|
| 127 |
+ end_byte: int
|
|
| 128 |
+ name: str
|
|
| 129 |
+ |
|
| 130 |
+ |
|
| 131 |
+@dataclass
|
|
| 132 |
+class PlainSpan(Span):
|
|
| 102 | 133 |
name: str = field(default='plain', init=False)
|
| 103 | 134 |
|
| 104 | 135 |
|
| 105 | 136 |
@dataclass
|
| 106 |
-class StrongSpan(StyleObject):
|
|
| 137 |
+class StrongSpan(Span):
|
|
| 107 | 138 |
name: str = field(default='strong', init=False)
|
| 108 | 139 |
|
| 109 | 140 |
|
| 110 | 141 |
@dataclass
|
| 111 |
-class EmphasisSpan(StyleObject):
|
|
| 142 |
+class EmphasisSpan(Span):
|
|
| 112 | 143 |
name: str = field(default='emphasis', init=False)
|
| 113 | 144 |
|
| 114 | 145 |
|
| 115 | 146 |
@dataclass
|
| 116 |
-class PreTextSpan(StyleObject):
|
|
| 147 |
+class PreTextSpan(Span):
|
|
| 117 | 148 |
name: str = field(default='pre', init=False)
|
| 118 | 149 |
|
| 119 | 150 |
|
| 120 | 151 |
@dataclass
|
| 121 |
-class StrikeSpan(StyleObject):
|
|
| 152 |
+class StrikeSpan(Span):
|
|
| 122 | 153 |
name: str = field(default='strike', init=False)
|
| 123 | 154 |
|
| 124 | 155 |
|
| ... | ... | @@ -133,18 +164,33 @@ class StrikeSpan(StyleObject): |
| 133 | 164 |
@dataclass
|
| 134 | 165 |
class ParsingResult:
|
| 135 | 166 |
text: str
|
| 136 |
- blocks: list
|
|
| 167 |
+ blocks: list[Block]
|
|
| 168 |
+ |
|
| 137 | 169 |
|
| 170 |
+def find_byte_index(text: str, index: int):
|
|
| 171 |
+ byte_index = -1
|
|
| 172 |
+ for index_, c in enumerate(text):
|
|
| 173 |
+ byte_index += len(c.encode())
|
|
| 174 |
+ if index == index_:
|
|
| 175 |
+ return byte_index
|
|
| 176 |
+ |
|
| 177 |
+ raise ValueError('index not in string: %s, %s' % (text, index))
|
|
| 178 |
+ |
|
| 179 |
+ |
|
| 180 |
+def process(text: Union[str, bytes], nested: bool = False) -> ParsingResult:
|
|
| 181 |
+ if isinstance(text, bytes):
|
|
| 182 |
+ text = text.decode()
|
|
| 138 | 183 |
|
| 139 |
-def process(text, nested=False):
|
|
| 140 | 184 |
blocks = _parse_blocks(text, nested)
|
| 141 | 185 |
for block in blocks:
|
| 142 | 186 |
if isinstance(block, PlainBlock):
|
| 143 | 187 |
offset = 0
|
| 188 |
+ offset_bytes = 0
|
|
| 144 | 189 |
for line in block.text.splitlines(keepends=True):
|
| 145 |
- block.spans += _parse_line(line, offset)
|
|
| 146 |
- block.uris += _parse_uris(line, offset)
|
|
| 190 |
+ block.spans += _parse_line(line, offset, offset_bytes)
|
|
| 191 |
+ block.uris += _parse_uris(line, offset, offset_bytes)
|
|
| 147 | 192 |
offset += len(line)
|
| 193 |
+ offset_bytes += len(line.encode())
|
|
| 148 | 194 |
|
| 149 | 195 |
if isinstance(block, QuoteBlock):
|
| 150 | 196 |
result = process(block.unquote(), nested=True)
|
| ... | ... | @@ -153,8 +199,8 @@ def process(text, nested=False): |
| 153 | 199 |
return ParsingResult(text, blocks)
|
| 154 | 200 |
|
| 155 | 201 |
|
| 156 |
-def _parse_blocks(text, nested):
|
|
| 157 |
- blocks = []
|
|
| 202 |
+def _parse_blocks(text: str, nested: bool) -> list[Block]:
|
|
| 203 |
+ blocks: list[Block] = []
|
|
| 158 | 204 |
text_len = len(text)
|
| 159 | 205 |
last_end_pos = 0
|
| 160 | 206 |
|
| ... | ... | @@ -162,9 +208,10 @@ def _parse_blocks(text, nested): |
| 162 | 208 |
|
| 163 | 209 |
for match in rx.finditer(text):
|
| 164 | 210 |
if match.start() != last_end_pos:
|
| 165 |
- blocks.append(PlainBlock(start=last_end_pos,
|
|
| 166 |
- end=match.start(),
|
|
| 167 |
- text=text[last_end_pos:match.start()]))
|
|
| 211 |
+ blocks.append(PlainBlock(
|
|
| 212 |
+ start=last_end_pos,
|
|
| 213 |
+ end=match.start(),
|
|
| 214 |
+ text=text[last_end_pos:match.start()]))
|
|
| 168 | 215 |
|
| 169 | 216 |
last_end_pos = match.end()
|
| 170 | 217 |
group_dict = match.groupdict()
|
| ... | ... | @@ -176,17 +223,19 @@ def _parse_blocks(text, nested): |
| 176 | 223 |
blocks.append(PreBlock.from_match(match))
|
| 177 | 224 |
|
| 178 | 225 |
if last_end_pos != text_len:
|
| 179 |
- blocks.append(PlainBlock(start=last_end_pos,
|
|
| 180 |
- end=text_len,
|
|
| 181 |
- text=text[last_end_pos:]))
|
|
| 226 |
+ blocks.append(PlainBlock(
|
|
| 227 |
+ start=last_end_pos,
|
|
| 228 |
+ end=text_len,
|
|
| 229 |
+ text=text[last_end_pos:]))
|
|
| 230 |
+ |
|
| 182 | 231 |
return blocks
|
| 183 | 232 |
|
| 184 | 233 |
|
| 185 |
-def _parse_line(line, offset):
|
|
| 186 |
- index = 0
|
|
| 234 |
+def _parse_line(line: str, offset: int, offset_bytes: int) -> list[Span]:
|
|
| 235 |
+ index: int = 0
|
|
| 187 | 236 |
length = len(line)
|
| 188 |
- stack = []
|
|
| 189 |
- spans = []
|
|
| 237 |
+ stack: list[tuple[str, int]] = []
|
|
| 238 |
+ spans: list[Span] = []
|
|
| 190 | 239 |
|
| 191 | 240 |
while index < length:
|
| 192 | 241 |
sd = line[index]
|
| ... | ... | @@ -204,7 +253,11 @@ def _parse_line(line, offset): |
| 204 | 253 |
|
| 205 | 254 |
if is_valid_start:
|
| 206 | 255 |
if sd == PRE:
|
| 207 |
- index = _handle_pre_span(line, index, offset, spans)
|
|
| 256 |
+ index = _handle_pre_span(line,
|
|
| 257 |
+ index,
|
|
| 258 |
+ offset,
|
|
| 259 |
+ offset_bytes,
|
|
| 260 |
+ spans)
|
|
| 208 | 261 |
continue
|
| 209 | 262 |
|
| 210 | 263 |
stack.append((sd, index))
|
| ... | ... | @@ -222,27 +275,46 @@ def _parse_line(line, offset): |
| 222 | 275 |
continue
|
| 223 | 276 |
|
| 224 | 277 |
start_pos = _find_span_start_position(sd, stack)
|
| 225 |
- spans.append(_make_span(line, sd, start_pos, index, offset))
|
|
| 278 |
+ spans.append(_make_span(line,
|
|
| 279 |
+ sd,
|
|
| 280 |
+ start_pos,
|
|
| 281 |
+ index,
|
|
| 282 |
+ offset,
|
|
| 283 |
+ offset_bytes))
|
|
| 226 | 284 |
|
| 227 | 285 |
index += 1
|
| 228 | 286 |
|
| 229 | 287 |
return spans
|
| 230 | 288 |
|
| 231 | 289 |
|
| 232 |
-def _parse_uris(line, offset):
|
|
| 233 |
- uris = []
|
|
| 290 |
+def _parse_uris(line: str, offset: int, offset_bytes: int) -> list[BaseUri]:
|
|
| 291 |
+ uris: list[BaseUri] = []
|
|
| 292 |
+ |
|
| 293 |
+ def make(match: Match[str], is_address: bool) -> BaseUri:
|
|
| 294 |
+ return _make_link(line,
|
|
| 295 |
+ match.start(),
|
|
| 296 |
+ match.end() - 1,
|
|
| 297 |
+ offset,
|
|
| 298 |
+ offset_bytes,
|
|
| 299 |
+ is_address)
|
|
| 300 |
+ |
|
| 234 | 301 |
for match in URI_RX.finditer(line):
|
| 235 |
- uri = _make_uri(line, match.start(), match.end(), offset)
|
|
| 302 |
+ uri = make(match, False)
|
|
| 236 | 303 |
uris.append(uri)
|
| 237 | 304 |
|
| 238 | 305 |
for match in ADDRESS_RX.finditer(line):
|
| 239 |
- uri = _make_address(line, match.start(), match.end(), offset)
|
|
| 306 |
+ uri = make(match, True)
|
|
| 240 | 307 |
uris.append(uri)
|
| 241 | 308 |
|
| 242 | 309 |
return uris
|
| 243 | 310 |
|
| 244 | 311 |
|
| 245 |
-def _handle_pre_span(line, index, offset, spans):
|
|
| 312 |
+def _handle_pre_span(line: str,
|
|
| 313 |
+ index: int,
|
|
| 314 |
+ offset: int,
|
|
| 315 |
+ offset_bytes: int,
|
|
| 316 |
+ spans: list[Span]) -> int:
|
|
| 317 |
+ |
|
| 246 | 318 |
# Scan ahead for the end
|
| 247 | 319 |
end = line.find(PRE, index + 1)
|
| 248 | 320 |
if end == -1:
|
| ... | ... | @@ -252,40 +324,69 @@ def _handle_pre_span(line, index, offset, spans): |
| 252 | 324 |
# empty span
|
| 253 | 325 |
return index + 1
|
| 254 | 326 |
|
| 255 |
- spans.append(_make_span(line, PRE, index, end, offset))
|
|
| 327 |
+ spans.append(_make_span(line, PRE, index, end, offset, offset_bytes))
|
|
| 256 | 328 |
return end + 1
|
| 257 | 329 |
|
| 258 | 330 |
|
| 259 |
-def _make_span(line, sd, start, end, offset):
|
|
| 331 |
+def _make_span(line: str,
|
|
| 332 |
+ sd: str,
|
|
| 333 |
+ start: int,
|
|
| 334 |
+ end: int,
|
|
| 335 |
+ offset: int,
|
|
| 336 |
+ offset_bytes: int) -> Span:
|
|
| 337 |
+ |
|
| 260 | 338 |
text = line[start:end + 1]
|
| 339 |
+ |
|
| 340 |
+ start_byte = find_byte_index(line, start) + offset_bytes
|
|
| 341 |
+ end_byte = find_byte_index(line, end) + offset_bytes + 1
|
|
| 342 |
+ |
|
| 261 | 343 |
start += offset
|
| 262 | 344 |
end += offset + 1
|
| 263 | 345 |
span_class = SPAN_CLS_DICT.get(sd)
|
| 264 |
- return span_class(start=start, end=end, text=text)
|
|
| 346 |
+ assert span_class is not None
|
|
| 265 | 347 |
|
| 348 |
+ return span_class(start=start,
|
|
| 349 |
+ start_byte=start_byte,
|
|
| 350 |
+ end=end,
|
|
| 351 |
+ end_byte=end_byte,
|
|
| 352 |
+ text=text)
|
|
| 266 | 353 |
|
| 267 |
-def _make_uri(line, start, end, offset):
|
|
| 268 |
- text = line[start:end]
|
|
| 269 |
- start += offset
|
|
| 270 |
- end += offset
|
|
| 271 |
- return Uri(start=start, end=end, text=text)
|
|
| 272 | 354 |
|
| 355 |
+def _make_link(line: str,
|
|
| 356 |
+ start: int,
|
|
| 357 |
+ end: int,
|
|
| 358 |
+ offset: int,
|
|
| 359 |
+ offset_bytes: int,
|
|
| 360 |
+ is_address: bool) -> BaseUri:
|
|
| 361 |
+ |
|
| 362 |
+ text = line[start:end + 1]
|
|
| 363 |
+ |
|
| 364 |
+ start_byte = find_byte_index(line, start) + offset_bytes
|
|
| 365 |
+ end_byte = find_byte_index(line, end) + offset_bytes + 1
|
|
| 273 | 366 |
|
| 274 |
-def _make_address(line, start, end, offset):
|
|
| 275 |
- text = line[start:end]
|
|
| 276 | 367 |
start += offset
|
| 277 |
- end += offset
|
|
| 368 |
+ end += offset + 1
|
|
| 369 |
+ |
|
| 370 |
+ if not is_address:
|
|
| 371 |
+ cls_ = Uri
|
|
| 372 |
+ |
|
| 373 |
+ elif text.startswith('xmpp'):
|
|
| 374 |
+ cls_ = XMPPAddress
|
|
| 278 | 375 |
|
| 279 |
- if text.startswith('xmpp'):
|
|
| 280 |
- return XMPPAddress(start=start, end=end, text=text)
|
|
| 376 |
+ elif text.startswith('mailto'):
|
|
| 377 |
+ cls_ = MailAddress
|
|
| 281 | 378 |
|
| 282 |
- if text.startswith('mailto'):
|
|
| 283 |
- return MailAddress(start=start, end=end, text=text)
|
|
| 379 |
+ else:
|
|
| 380 |
+ cls_ = Address
|
|
| 284 | 381 |
|
| 285 |
- return Address(start=start, end=end, text=text)
|
|
| 382 |
+ return cls_(start=start,
|
|
| 383 |
+ start_byte=start_byte,
|
|
| 384 |
+ end=end,
|
|
| 385 |
+ end_byte=end_byte,
|
|
| 386 |
+ text=text)
|
|
| 286 | 387 |
|
| 287 | 388 |
|
| 288 |
-def _is_span_empty(sd, index, stack):
|
|
| 389 |
+def _is_span_empty(sd: str, index: int, stack: list[tuple[str, int]]) -> bool:
|
|
| 289 | 390 |
start_sd = stack[-1][SD]
|
| 290 | 391 |
if start_sd != sd:
|
| 291 | 392 |
return False
|
| ... | ... | @@ -294,7 +395,7 @@ def _is_span_empty(sd, index, stack): |
| 294 | 395 |
return pos == index - 1
|
| 295 | 396 |
|
| 296 | 397 |
|
| 297 |
-def _find_span_start_position(sd, stack):
|
|
| 398 |
+def _find_span_start_position(sd: str, stack: list[tuple[str, int]]) -> int:
|
|
| 298 | 399 |
while stack:
|
| 299 | 400 |
start_sd, pos = stack.pop()
|
| 300 | 401 |
if start_sd == sd:
|
| ... | ... | @@ -303,7 +404,7 @@ def _find_span_start_position(sd, stack): |
| 303 | 404 |
raise ValueError('Unable to find opening span')
|
| 304 | 405 |
|
| 305 | 406 |
|
| 306 |
-def _is_valid_span_start(line, index):
|
|
| 407 |
+def _is_valid_span_start(line: str, index: int) -> bool:
|
|
| 307 | 408 |
'''
|
| 308 | 409 |
https://xmpp.org/extensions/xep-0393.html#span
|
| 309 | 410 |
|
| ... | ... | @@ -328,7 +429,7 @@ def _is_valid_span_start(line, index): |
| 328 | 429 |
return char in VALID_SPAN_START
|
| 329 | 430 |
|
| 330 | 431 |
|
| 331 |
-def _is_valid_span_end(line, index):
|
|
| 432 |
+def _is_valid_span_end(line: str, index: int) -> bool:
|
|
| 332 | 433 |
'''
|
| 333 | 434 |
https://xmpp.org/extensions/xep-0393.html#span
|
| 334 | 435 |
|
| ... | ... | @@ -35,7 +35,7 @@ |
| 35 | 35 |
from gajim.gui_menu_builder import get_conv_uri_context_menu
|
| 36 | 36 |
|
| 37 | 37 |
from ..util import get_cursor
|
| 38 |
-from ..util import make_pango_attribute
|
|
| 38 |
+from ..util import make_pango_attributes
|
|
| 39 | 39 |
|
| 40 | 40 |
log = logging.getLogger('gajim.gui.conversaion.plain_widget')
|
| 41 | 41 |
|
| ... | ... | @@ -95,17 +95,17 @@ def _on_populate_popup(self, label: Gtk.Label, menu: Gtk.Menu) -> None: |
| 95 | 95 |
menu.show_all()
|
| 96 | 96 |
|
| 97 | 97 |
def print_text_with_styling(self, block: PlainBlock) -> None:
|
| 98 |
- text = GLib.markup_escape_text(block.text.strip())
|
|
| 98 |
+ text = ''
|
|
| 99 |
+ after = GLib.markup_escape_text(block.text.strip())
|
|
| 99 | 100 |
for uri in block.uris:
|
| 100 |
- text = text.replace(uri.text, uri.get_markup_string())
|
|
| 101 |
- |
|
| 102 |
- attr_list = Pango.AttrList()
|
|
| 103 |
- for span in block.spans:
|
|
| 104 |
- attr = make_pango_attribute(span.name, span.start, span.end)
|
|
| 105 |
- attr_list.insert(attr)
|
|
| 101 |
+ uri_escaped = GLib.markup_escape_text(uri.text)
|
|
| 102 |
+ before, _, after = after.partition(uri_escaped)
|
|
| 103 |
+ text += before
|
|
| 104 |
+ text += uri.get_markup_string()
|
|
| 105 |
+ text += after
|
|
| 106 | 106 |
|
| 107 | 107 |
self.set_markup(text)
|
| 108 |
- self.set_attributes(attr_list)
|
|
| 108 |
+ self.set_attributes(make_pango_attributes(block))
|
|
| 109 | 109 |
|
| 110 | 110 |
def update_text_tags(self) -> None:
|
| 111 | 111 |
pass
|
| ... | ... | @@ -55,6 +55,7 @@ |
| 55 | 55 |
from gajim.common.const import Display
|
| 56 | 56 |
from gajim.common.const import StyleAttr
|
| 57 | 57 |
from gajim.common.nec import EventHelper as CommonEventHelper
|
| 58 |
+from gajim.common.styling import PlainBlock
|
|
| 58 | 59 |
|
| 59 | 60 |
from .const import GajimIconSet
|
| 60 | 61 |
from .const import WINDOW_MODULES
|
| ... | ... | @@ -888,19 +889,27 @@ def refresh(self) -> None: |
| 888 | 889 |
self.set_tooltip_text(_('Account: %s') % label)
|
| 889 | 890 |
|
| 890 | 891 |
|
| 891 |
-def make_pango_attribute(name: str, start: int, end: int) -> Pango.Attribute:
|
|
| 892 |
+def make_pango_attributes(block: PlainBlock) -> Pango.AttrList:
|
|
| 893 |
+ attrlist = Pango.AttrList()
|
|
| 894 |
+ for span in block.spans:
|
|
| 895 |
+ attr = get_style_attribute_with_name(span.name)
|
|
| 896 |
+ attr.start_index = span.start_byte
|
|
| 897 |
+ attr.end_index = span.end_byte
|
|
| 898 |
+ attrlist.insert(attr)
|
|
| 899 |
+ return attrlist
|
|
| 900 |
+ |
|
| 901 |
+ |
|
| 902 |
+def get_style_attribute_with_name(name: str) -> Pango.Attribute:
|
|
| 892 | 903 |
if name == 'strong':
|
| 893 |
- attr = Pango.attr_weight_new(Pango.Weight.BOLD)
|
|
| 904 |
+ return Pango.attr_weight_new(Pango.Weight.BOLD)
|
|
| 905 |
+ |
|
| 894 | 906 |
if name == 'strike':
|
| 895 |
- attr = Pango.attr_strikethrough_new(True)
|
|
| 907 |
+ return Pango.attr_strikethrough_new(True)
|
|
| 908 |
+ |
|
| 896 | 909 |
if name == 'emphasis':
|
| 897 |
- attr = Pango.attr_style_new(Pango.Style.ITALIC)
|
|
| 898 |
- if name == 'pre':
|
|
| 899 |
- attr = Pango.attr_family_new('monospace')
|
|
| 910 |
+ return Pango.attr_style_new(Pango.Style.ITALIC)
|
|
| 900 | 911 |
|
| 901 |
- else:
|
|
| 902 |
- ValueError('unknown attribute %s', name)
|
|
| 912 |
+ if name == 'pre':
|
|
| 913 |
+ return Pango.attr_family_new('monospace')
|
|
| 903 | 914 |
|
| 904 |
- attr.start_index = start
|
|
| 905 |
- attr.end_index = end
|
|
| 906 |
- return attr
|
|
| 915 |
+ raise ValueError('unknown attribute %s' % name)
|
| ... | ... | @@ -18,8 +18,8 @@ |
| 18 | 18 |
'input': '_no pre `with *children*`_',
|
| 19 | 19 |
'tokens': [
|
| 20 | 20 |
PlainBlock(start=0, end=26, text='_no pre `with *children*`_', spans=[
|
| 21 |
- PreTextSpan(start=8, end=25, text='`with *children*`'),
|
|
| 22 |
- EmphasisSpan(start=0, end=26, text='_no pre `with *children*`_')
|
|
| 21 |
+ PreTextSpan(start=8, start_byte=8, end=25, end_byte=25, text='`with *children*`'),
|
|
| 22 |
+ EmphasisSpan(start=0, start_byte=0, end=26, end_byte=26, text='_no pre `with *children*`_')
|
|
| 23 | 23 |
])
|
| 24 | 24 |
]
|
| 25 | 25 |
},
|
| ... | ... | @@ -28,9 +28,9 @@ |
| 28 | 28 |
'input': '_*~children~*_',
|
| 29 | 29 |
'tokens': [
|
| 30 | 30 |
PlainBlock(start=0, end=14, text='_*~children~*_', spans=[
|
| 31 |
- StrikeSpan(start=2, end=12, text='~children~'),
|
|
| 32 |
- StrongSpan(start=1, end=13, text='*~children~*'),
|
|
| 33 |
- EmphasisSpan(start=0, end=14, text='_*~children~*_'),
|
|
| 31 |
+ StrikeSpan(start=2, start_byte=2, end=12, end_byte=12, text='~children~'),
|
|
| 32 |
+ StrongSpan(start=1, start_byte=1, end=13, end_byte=13, text='*~children~*'),
|
|
| 33 |
+ EmphasisSpan(start=0, start_byte=0, end=14, end_byte=14, text='_*~children~*_'),
|
|
| 34 | 34 |
])
|
| 35 | 35 |
]
|
| 36 | 36 |
},
|
| ... | ... | @@ -39,10 +39,10 @@ |
| 39 | 39 |
'input': '*strong* _emph_~strike~ `pre`',
|
| 40 | 40 |
'tokens': [
|
| 41 | 41 |
PlainBlock(start=0, end=30, text='*strong* _emph_~strike~ `pre`', spans=[
|
| 42 |
- StrongSpan(start=0, end=8, text='*strong*'),
|
|
| 43 |
- EmphasisSpan(start=9, end=15, text='_emph_'),
|
|
| 44 |
- StrikeSpan(start=15, end=23, text='~strike~'),
|
|
| 45 |
- PreTextSpan(start=25, end=30, text='`pre`')
|
|
| 42 |
+ StrongSpan(start=0, start_byte=0, end=8, end_byte=8, text='*strong*'),
|
|
| 43 |
+ EmphasisSpan(start=9, start_byte=9, end=15, end_byte=15, text='_emph_'),
|
|
| 44 |
+ StrikeSpan(start=15, start_byte=15, end=23, end_byte=23, text='~strike~'),
|
|
| 45 |
+ PreTextSpan(start=25, start_byte=25, end=30, end_byte=30, text='`pre`')
|
|
| 46 | 46 |
])
|
| 47 | 47 |
]
|
| 48 | 48 |
},
|
| ... | ... | @@ -51,7 +51,7 @@ |
| 51 | 51 |
'input': '*strong*plain*',
|
| 52 | 52 |
'tokens': [
|
| 53 | 53 |
PlainBlock(start=0, end=14, text='*strong*plain*', spans=[
|
| 54 |
- StrongSpan(start=0, end=8, text='*strong*')
|
|
| 54 |
+ StrongSpan(start=0, start_byte=0, end=8, end_byte=8, text='*strong*')
|
|
| 55 | 55 |
])
|
| 56 | 56 |
]
|
| 57 | 57 |
},
|
| ... | ... | @@ -63,6 +63,26 @@ |
| 63 | 63 |
]
|
| 64 | 64 |
},
|
| 65 | 65 |
|
| 66 |
+ 'byte pos is different': {
|
|
| 67 |
+ 'input': '*ö* *öö*',
|
|
| 68 |
+ 'tokens': [
|
|
| 69 |
+ PlainBlock(start=0, end=8, text='*ö* *öö*', spans=[
|
|
| 70 |
+ StrongSpan(start=0, start_byte=0, end=3, end_byte=4, text='*ö*'),
|
|
| 71 |
+ StrongSpan(start=4, start_byte=5, end=8, end_byte=11, text='*öö*')
|
|
| 72 |
+ ])
|
|
| 73 |
+ ]
|
|
| 74 |
+ },
|
|
| 75 |
+ |
|
| 76 |
+ 'byte pos is different with multiple blocks': {
|
|
| 77 |
+ 'input': '```\npre\n```\n*pláin*',
|
|
| 78 |
+ 'tokens': [
|
|
| 79 |
+ PreBlock(start=0, end=12, text='```\npre\n```\n'),
|
|
| 80 |
+ PlainBlock(start=12, end=19, text='*pláin*', spans=[
|
|
| 81 |
+ StrongSpan(start=0, start_byte=0, end=7, end_byte=8, text='*pláin*')
|
|
| 82 |
+ ])
|
|
| 83 |
+ ]
|
|
| 84 |
+ },
|
|
| 85 |
+ |
|
| 66 | 86 |
'end span only': {
|
| 67 | 87 |
'input': 'not strong*',
|
| 68 | 88 |
'tokens': [
|
| ... | ... | @@ -102,7 +122,7 @@ |
| 102 | 122 |
'input': '* plain *strong*',
|
| 103 | 123 |
'tokens': [
|
| 104 | 124 |
PlainBlock(start=0, end=16, text='* plain *strong*', spans=[
|
| 105 |
- StrongSpan(start=8, end=16, text='*strong*')
|
|
| 125 |
+ StrongSpan(start=8, start_byte=8, end=16, end_byte=16, text='*strong*')
|
|
| 106 | 126 |
])
|
| 107 | 127 |
]
|
| 108 | 128 |
},
|
| ... | ... | @@ -111,7 +131,7 @@ |
| 111 | 131 |
'input': '*this is *uneven*',
|
| 112 | 132 |
'tokens': [
|
| 113 | 133 |
PlainBlock(start=0, end=17, text='*this is *uneven*', spans=[
|
| 114 |
- StrongSpan(start=9, end=17, text='*uneven*')
|
|
| 134 |
+ StrongSpan(start=9, start_byte=9, end=17, end_byte=17, text='*uneven*')
|
|
| 115 | 135 |
])
|
| 116 | 136 |
]
|
| 117 | 137 |
},
|
| ... | ... | @@ -120,7 +140,7 @@ |
| 120 | 140 |
'input': '*this cannot _overlap*_',
|
| 121 | 141 |
'tokens': [
|
| 122 | 142 |
PlainBlock(start=0, end=23, text='*this cannot _overlap*_', spans=[
|
| 123 |
- StrongSpan(start=0, end=22, text='*this cannot _overlap*')
|
|
| 143 |
+ StrongSpan(start=0, start_byte=0, end=22, end_byte=22, text='*this cannot _overlap*')
|
|
| 124 | 144 |
])
|
| 125 | 145 |
]
|
| 126 | 146 |
},
|
| ... | ... | @@ -223,7 +243,7 @@ |
| 223 | 243 |
'input': 'some kind of link http://foo.com/blah_blah',
|
| 224 | 244 |
'tokens': [
|
| 225 | 245 |
PlainBlock(start=0, end=42, text='some kind of link http://foo.com/blah_blah', spans=[], uris=[
|
| 226 |
- Uri(start=18, end=42, text='http://foo.com/blah_blah')
|
|
| 246 |
+ Uri(start=18, start_byte=18, end=42, end_byte=42, text='http://foo.com/blah_blah')
|
|
| 227 | 247 |
])
|
| 228 | 248 |
]
|
| 229 | 249 |
},
|
| ... | ... | @@ -232,7 +252,7 @@ |
| 232 | 252 |
'input': 'some kind of link http://foo.com/blah_blah,',
|
| 233 | 253 |
'tokens': [
|
| 234 | 254 |
PlainBlock(start=0, end=43, text='some kind of link http://foo.com/blah_blah,', spans=[], uris=[
|
| 235 |
- Uri(start=18, end=42, text='http://foo.com/blah_blah')
|
|
| 255 |
+ Uri(start=18, start_byte=18, end=42, end_byte=42, text='http://foo.com/blah_blah')
|
|
| 236 | 256 |
])
|
| 237 | 257 |
]
|
| 238 | 258 |
},
|
| ... | ... | @@ -241,9 +261,19 @@ |
| 241 | 261 |
'input': 'some *kind* of link http://foo.com/blah_blah',
|
| 242 | 262 |
'tokens': [
|
| 243 | 263 |
PlainBlock(start=0, end=44, text='some *kind* of link http://foo.com/blah_blah', spans=[
|
| 244 |
- StrongSpan(start=5, end=11, text='*kind*')
|
|
| 264 |
+ StrongSpan(start=5, start_byte=5, end=11, end_byte=11, text='*kind*')
|
|
| 245 | 265 |
], uris=[
|
| 246 |
- Uri(start=20, end=44, text='http://foo.com/blah_blah')
|
|
| 266 |
+ Uri(start=20, start_byte=20, end=44, end_byte=44, text='http://foo.com/blah_blah')
|
|
| 267 |
+ ])
|
|
| 268 |
+ ]
|
|
| 269 |
+ },
|
|
| 270 |
+ |
|
| 271 |
+ 'plain with multiple uris': {
|
|
| 272 |
+ 'input': 'some http://foo.com/blah_blah and http://foo.com/blah_blah/123',
|
|
| 273 |
+ 'tokens': [
|
|
| 274 |
+ PlainBlock(start=0, end=62, text='some http://foo.com/blah_blah and http://foo.com/blah_blah/123', spans=[], uris=[
|
|
| 275 |
+ Uri(start=5, start_byte=5, end=29, end_byte=29, text='http://foo.com/blah_blah'),
|
|
| 276 |
+ Uri(start=34, start_byte=34, end=62, end_byte=62, text='http://foo.com/blah_blah/123')
|
|
| 247 | 277 |
])
|
| 248 | 278 |
]
|
| 249 | 279 |
},
|