Philipp Hörist pushed to branch master at gajim / gajim

Commits:

4 changed files:

Changes:

  • gajim/common/styling.py
    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
     
    

  • gajim/gtk/conversation/plain_widget.py
    ... ... @@ -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
    

  • gajim/gtk/util.py
    ... ... @@ -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)

  • test/no_gui/test_styling.py
    ... ... @@ -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
         },