diff --git a/CHANGELOG.md b/CHANGELOG.md index 4218362e..95cccbb3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [10.16.1] - 2021-12-15 + +### Fixed + +- Fixed issues with overlapping tags https://github.com/willmcgugan/rich/issues/1755 + ## [10.16.0] - 2021-12-12 ### Fixed diff --git a/pyproject.toml b/pyproject.toml index 7e219d48..c26c6b1d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,7 @@ name = "rich" homepage = "https://github.com/willmcgugan/rich" documentation = "https://rich.readthedocs.io/en/latest/" -version = "10.16.0" +version = "10.16.1" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" authors = ["Will McGugan "] license = "MIT" diff --git a/rich/text.py b/rich/text.py index b277f6c1..8061f8d1 100644 --- a/rich/text.py +++ b/rich/text.py @@ -1,7 +1,7 @@ import re from functools import partial, reduce from math import gcd -from operator import attrgetter, itemgetter +from operator import itemgetter from rich.emoji import EmojiVariant from typing import ( TYPE_CHECKING, @@ -1033,6 +1033,7 @@ class Text(JupyterMixin): Lines: New RichText instances between offsets. """ _offsets = list(offsets) + if not _offsets: return Lines([self.copy()]) @@ -1056,33 +1057,46 @@ class Text(JupyterMixin): ) if not self._spans: return new_lines - order = {span: span_index for span_index, span in enumerate(self._spans)} - span_stack = sorted(self._spans, key=attrgetter("start"), reverse=True) - pop = span_stack.pop - push = span_stack.append + _line_appends = [line._spans.append for line in new_lines._lines] + line_count = len(line_ranges) _Span = Span - get_order = order.__getitem__ - for line, (start, end) in zip(new_lines, line_ranges): - if not span_stack: - break - append_span = line._spans.append - position = len(span_stack) - 1 - while span_stack[position].start < end: - span = pop(position) - add_span, remaining_span = span.split(end) - if remaining_span: - push(remaining_span) - order[remaining_span] = order[span] - span_start, span_end, span_style = add_span - line_span = _Span(span_start - start, span_end - start, span_style) - order[line_span] = order[span] - append_span(line_span) - position -= 1 - if position < 0 or not span_stack: - break # pragma: no cover - line._spans.sort(key=get_order) + for span_start, span_end, style in self._spans: + + lower_bound = 0 + upper_bound = line_count + start_line_no = (lower_bound + upper_bound) // 2 + + while True: + line_start, line_end = line_ranges[start_line_no] + if span_start < line_start: + upper_bound = start_line_no - 1 + elif span_start > line_end: + lower_bound = start_line_no + 1 + else: + break + start_line_no = (lower_bound + upper_bound) // 2 + + end_line_no = lower_bound = start_line_no + upper_bound = line_count + + while True: + line_start, line_end = line_ranges[end_line_no] + if span_end < line_start: + upper_bound = end_line_no - 1 + elif span_end > line_end: + lower_bound = end_line_no + 1 + else: + break + end_line_no = (lower_bound + upper_bound) // 2 + + for line_no in range(start_line_no, end_line_no + 1): + line_start, line_end = line_ranges[line_no] + new_start = max(0, span_start - line_start) + new_end = min(span_end - line_start, line_end - line_start) + if new_end > new_start: + _line_appends[line_no](_Span(new_start, new_end, style)) return new_lines diff --git a/tests/test_segment.py b/tests/test_segment.py index dcf7b218..9aff243a 100644 --- a/tests/test_segment.py +++ b/tests/test_segment.py @@ -1,4 +1,4 @@ -import sys +from io import StringIO import pytest @@ -166,6 +166,34 @@ def test_divide(): ] +# https://github.com/willmcgugan/rich/issues/1755 +def test_divide_complex(): + MAP = ( + "[on orange4] [on green]XX[on orange4] \n" + " \n" + " \n" + " \n" + " [bright_red on black]Y[on orange4] \n" + "[on green]X[on orange4] [on green]X[on orange4] \n" + " [on green]X[on orange4] [on green]X\n" + "[on orange4] \n" + " [on green]XX[on orange4] \n" + ) + from rich.text import Text + from rich.console import Console + + text = Text.from_markup(MAP) + console = Console( + color_system="truecolor", width=30, force_terminal=True, file=StringIO() + ) + console.print(text) + result = console.file.getvalue() + + print(repr(result)) + expected = "\x1b[48;5;94m \x1b[0m\x1b[42mXX\x1b[0m\x1b[48;5;94m \x1b[0m\n\x1b[48;5;94m \x1b[0m\n\x1b[48;5;94m \x1b[0m\n\x1b[48;5;94m \x1b[0m\n\x1b[48;5;94m \x1b[0m\x1b[91;40mY\x1b[0m\x1b[91;48;5;94m \x1b[0m\n\x1b[91;42mX\x1b[0m\x1b[91;48;5;94m \x1b[0m\x1b[91;42mX\x1b[0m\x1b[91;48;5;94m \x1b[0m\n\x1b[91;48;5;94m \x1b[0m\x1b[91;42mX\x1b[0m\x1b[91;48;5;94m \x1b[0m\x1b[91;42mX\x1b[0m\n\x1b[91;48;5;94m \x1b[0m\n\x1b[91;48;5;94m \x1b[0m\x1b[91;42mXX\x1b[0m\x1b[91;48;5;94m \x1b[0m\n\n" + assert result == expected + + def test_divide_emoji(): bold = Style(bold=True) italic = Style(italic=True)