mirror of https://github.com/Textualize/rich.git
tabs to spaces, Console.markup applies to Table
This commit is contained in:
parent
9529453cce
commit
2a53a15284
16
CHANGELOG.md
16
CHANGELOG.md
|
@ -5,6 +5,22 @@ 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).
|
||||
|
||||
## [0.6.0] - Unreleased
|
||||
|
||||
### Added
|
||||
|
||||
- Added tab_size to Console and Text
|
||||
- Added protocol.is_renderable for runtime check
|
||||
|
||||
### Changed
|
||||
|
||||
- Console.markup attribute now effects Table
|
||||
- SeparatedConsoleRenderable and RichCast types
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed tabs breaking rendering by converting to spaces
|
||||
|
||||
## [0.5.0] - 2020-02-23
|
||||
|
||||
### Changed
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 1.2 MiB |
Binary file not shown.
Before Width: | Height: | Size: 401 KiB |
|
@ -2,7 +2,7 @@
|
|||
name = "rich"
|
||||
homepage = "https://github.com/willmcgugan/rich"
|
||||
documentation = "https://rich.readthedocs.io/en/latest/"
|
||||
version = "0.5.0"
|
||||
version = "0.6.0"
|
||||
description = "Render rich text, tables, syntax highlighting, markdown and more to the terminal"
|
||||
authors = ["Will McGugan <willmcgugan@gmail.com>"]
|
||||
license = "MIT"
|
||||
|
|
|
@ -108,18 +108,29 @@ class ConsoleOptions:
|
|||
return options
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class RichCast(Protocol):
|
||||
"""An object that may be 'cast' to a console renderable."""
|
||||
|
||||
def __rich__(self) -> Union["ConsoleRenderable", str]: # pragma: no cover
|
||||
...
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class ConsoleRenderable(Protocol):
|
||||
"""An object that supports the console protocol."""
|
||||
|
||||
def __console__(
|
||||
self, console: "Console", options: "ConsoleOptions"
|
||||
) -> Iterable[Union["ConsoleRenderable", Segment, str]]: # pragma: no cover
|
||||
) -> "RenderResult": # pragma: no cover
|
||||
...
|
||||
|
||||
|
||||
RenderableType = Union[ConsoleRenderable, Segment, str]
|
||||
RenderResult = Iterable[RenderableType]
|
||||
"""A type that may be rendered by Console."""
|
||||
RenderableType = Union[ConsoleRenderable, RichCast, str]
|
||||
|
||||
"""The result of calling a __console__ method."""
|
||||
RenderResult = Iterable[Union[RenderableType, Segment]]
|
||||
|
||||
|
||||
_null_highlighter = NullHighlighter()
|
||||
|
@ -246,6 +257,7 @@ class Console:
|
|||
file: IO = None,
|
||||
width: int = None,
|
||||
height: int = None,
|
||||
tab_size: int = 8,
|
||||
record: bool = False,
|
||||
markup: bool = True,
|
||||
log_time: bool = True,
|
||||
|
@ -259,6 +271,7 @@ class Console:
|
|||
self.file = file or sys.stdout
|
||||
self._width = width
|
||||
self._height = height
|
||||
self.tab_size = tab_size
|
||||
self.record = record
|
||||
self._markup = markup
|
||||
|
||||
|
@ -410,7 +423,9 @@ class Console:
|
|||
self._check_buffer()
|
||||
|
||||
def _render(
|
||||
self, renderable: RenderableType, options: Optional[ConsoleOptions]
|
||||
self,
|
||||
renderable: Union[RenderableType, Segment],
|
||||
options: Optional[ConsoleOptions],
|
||||
) -> Iterable[Segment]:
|
||||
"""Render an object in to an iterable of `Segment` instances.
|
||||
|
||||
|
@ -425,17 +440,15 @@ class Console:
|
|||
Returns:
|
||||
Iterable[Segment]: An iterable of segments that may be rendered.
|
||||
"""
|
||||
render_iterable: Iterable[RenderableType]
|
||||
render_options = options or self.options
|
||||
render_iterable: RenderResult
|
||||
if isinstance(renderable, Segment):
|
||||
yield renderable
|
||||
return
|
||||
elif isinstance(renderable, ConsoleRenderable):
|
||||
render_options = options or self.options
|
||||
if isinstance(renderable, ConsoleRenderable):
|
||||
render_iterable = renderable.__console__(self, render_options)
|
||||
elif isinstance(renderable, str):
|
||||
from .text import Text
|
||||
|
||||
yield from self._render(Text(renderable), render_options)
|
||||
yield from self.render(self.render_str(renderable), render_options)
|
||||
return
|
||||
else:
|
||||
raise errors.NotRenderableError(
|
||||
|
@ -522,20 +535,22 @@ class Console:
|
|||
)
|
||||
return lines
|
||||
|
||||
def render_str(self, text: str) -> "Text":
|
||||
def render_str(self, text: str, style: Union[str, Style] = "") -> "Text":
|
||||
"""Convert a string to a Text instance.
|
||||
|
||||
Args:
|
||||
text (str): Text to render.
|
||||
|
||||
style (Union[str, Style], optional): Style to apply to rendered text.
|
||||
Returns:
|
||||
ConsoleRenderable: Renderable object.
|
||||
|
||||
"""
|
||||
if self._markup:
|
||||
return markup.render(text)
|
||||
return markup.render(text, style=style)
|
||||
|
||||
return markup.render_text(text)
|
||||
from .text import Text
|
||||
|
||||
return Text(text, style=style)
|
||||
|
||||
def _get_style(self, name: str) -> Optional[Style]:
|
||||
"""Get a named style, or `None` if it doesn't exist.
|
||||
|
@ -681,10 +696,10 @@ class Console:
|
|||
check_text()
|
||||
append(renderable)
|
||||
elif isinstance(renderable, str):
|
||||
render_str = renderable
|
||||
renderable_str = renderable
|
||||
if emoji:
|
||||
render_str = _emoji_replace(render_str)
|
||||
render_text = self.render_str(render_str)
|
||||
renderable_str = _emoji_replace(renderable_str)
|
||||
render_text = self.render_str(renderable_str)
|
||||
append_text(_highlighter(render_text))
|
||||
elif isinstance(renderable, Text):
|
||||
append_text(renderable)
|
||||
|
|
|
@ -4,15 +4,16 @@ from .console import Console, ConsoleOptions, ConsoleRenderable, RenderResult
|
|||
|
||||
|
||||
class Constrain:
|
||||
"""Constrain the width of a renderable to a given number of characters.
|
||||
|
||||
Args:
|
||||
renderable (ConsoleRenderable): A renderable object.
|
||||
width (int, optional): The maximum width (in characters) to render. Defaults to 80.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, renderable: ConsoleRenderable, width: Optional[int] = 80
|
||||
) -> None:
|
||||
"""Constrain the width of a renderable to a given number of characters.
|
||||
|
||||
Args:
|
||||
renderable (ConsoleRenderable): A renderable object.
|
||||
width (int, optional): The maximum width (in characters) to render. Defaults to 80.
|
||||
"""
|
||||
self.renderable = renderable
|
||||
self.width = width
|
||||
|
||||
|
|
|
@ -59,11 +59,6 @@ def _parse(markup: str) -> Iterable[Tuple[Optional[str], Optional[str]]]:
|
|||
yield markup[position:], None
|
||||
|
||||
|
||||
def render_text(markup: str, style: Union[str, Style] = "") -> Text:
|
||||
"""Convert markup to Text instance."""
|
||||
return Text(markup, style=style)
|
||||
|
||||
|
||||
def render(markup: str, style: Union[str, Style] = "") -> Text:
|
||||
"""Render console markup in to a Text instance.
|
||||
|
||||
|
|
|
@ -45,10 +45,6 @@ class Padding:
|
|||
if len(pad) == 2:
|
||||
pad_top, pad_right = cast(Tuple[int, int], pad)
|
||||
return (pad_top, pad_right, pad_top, pad_right)
|
||||
if len(pad) == 3:
|
||||
raise ValueError(
|
||||
f"1, 2 or 4 integers required for padding; {len(pad)} given"
|
||||
)
|
||||
if len(pad) == 4:
|
||||
top, right, bottom, left = cast(Tuple[int, int, int, int], pad)
|
||||
return (top, right, bottom, left)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
from operator import itemgetter
|
||||
from typing import Iterable, NamedTuple, TYPE_CHECKING
|
||||
from typing import Iterable, NamedTuple, TYPE_CHECKING, Union
|
||||
|
||||
from . import errors
|
||||
from .segment import Segment
|
||||
|
@ -37,7 +37,9 @@ class RenderWidth(NamedTuple):
|
|||
return RenderWidth(min(minimum, width), min(maximum, width))
|
||||
|
||||
@classmethod
|
||||
def get(cls, renderable: "RenderableType", max_width: int) -> "RenderWidth":
|
||||
def get(
|
||||
cls, renderable: Union["RenderableType", "Segment"], max_width: int
|
||||
) -> "RenderWidth":
|
||||
"""Get desired width for a renderable."""
|
||||
if hasattr(renderable, "__console__"):
|
||||
get_console_width = getattr(renderable, "__console_width__", None)
|
||||
|
|
|
@ -16,7 +16,7 @@ from .text import Text
|
|||
from ._tools import iter_first
|
||||
|
||||
WINDOWS = platform.system() == "Windows"
|
||||
DEFAULT_THEME = "dark"
|
||||
DEFAULT_THEME = "monokai"
|
||||
|
||||
|
||||
class Syntax:
|
||||
|
@ -31,6 +31,8 @@ class Syntax:
|
|||
start_line (int, optional): Starting number for line numbers. Defaults to 1.
|
||||
line_range (Tuple[int, int], optional): If given should be a tuple of the start and end line to render.
|
||||
highlight_lines (Set[int]): A set of line numbers to highlight.
|
||||
code_width: Width of code to render (not including line numbers), or ``None`` to use all available width.
|
||||
tab_size (int, optional): Size of tabs. Defaults to 4.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
|
@ -45,6 +47,7 @@ class Syntax:
|
|||
line_range: Tuple[int, int] = None,
|
||||
highlight_lines: Set[int] = None,
|
||||
code_width: Optional[int] = None,
|
||||
tab_size: int = 4,
|
||||
) -> None:
|
||||
self.code = code
|
||||
self.lexer_name = lexer_name
|
||||
|
@ -54,6 +57,7 @@ class Syntax:
|
|||
self.line_range = line_range
|
||||
self.highlight_lines = highlight_lines or set()
|
||||
self.code_width = code_width
|
||||
self.tab_size = tab_size
|
||||
|
||||
self._style_cache: Dict[Any, Style] = {}
|
||||
if not isinstance(theme, str) and issubclass(theme, PygmentsStyle):
|
||||
|
@ -76,6 +80,7 @@ class Syntax:
|
|||
start_line: int = 1,
|
||||
highlight_lines: Set[int] = None,
|
||||
code_width: Optional[int] = None,
|
||||
tab_size: int = 4,
|
||||
) -> "Syntax":
|
||||
"""Construct a Syntax object from a file.
|
||||
|
||||
|
@ -88,6 +93,8 @@ class Syntax:
|
|||
start_line (int, optional): Starting number for line numbers. Defaults to 1.
|
||||
line_range (Tuple[int, int], optional): If given should be a tuple of the start and end line to render.
|
||||
highlight_lines (Set[int]): A set of line numbers to highlight.
|
||||
code_width: Width of code to render (not including line numbers), or ``None`` to use all available width.
|
||||
tab_size (int, optional): Size of tabs. Defaults to 4.
|
||||
|
||||
Returns:
|
||||
[Syntax]: A Syntax object that may be printed to the console
|
||||
|
@ -139,8 +146,10 @@ class Syntax:
|
|||
try:
|
||||
lexer = get_lexer_by_name(lexer_name)
|
||||
except ClassNotFound:
|
||||
return Text(self.code, style=default_style)
|
||||
text = Text(style=default_style)
|
||||
return Text(
|
||||
self.code, justify="left", style=default_style, tab_size=self.tab_size
|
||||
)
|
||||
text = Text(justify="left", style=default_style, tab_size=self.tab_size)
|
||||
append = text.append
|
||||
_get_theme_style = self._get_theme_style
|
||||
for token_type, token in lexer.get_tokens(self.code):
|
||||
|
@ -257,32 +266,11 @@ class Syntax:
|
|||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
CODE = r"""
|
||||
def __init__(self):
|
||||
self.b = self.
|
||||
|
||||
"""
|
||||
# syntax = Syntax(CODE, "python", dedent=True, line_numbers=True, start_line=990)
|
||||
import sys
|
||||
from rich.console import Console
|
||||
|
||||
from time import time
|
||||
console = Console()
|
||||
|
||||
syntax = Syntax(CODE, "python", line_numbers=False, theme="monokai")
|
||||
syntax = Syntax.from_path(
|
||||
"rich/segment.py",
|
||||
theme="fruity",
|
||||
line_numbers=True,
|
||||
# line_range=(190, 300),
|
||||
highlight_lines={194},
|
||||
)
|
||||
console = Console(record=True)
|
||||
start = time()
|
||||
syntax = Syntax.from_path(sys.argv[1])
|
||||
console.print(syntax)
|
||||
elapsed = int((time() - start) * 1000)
|
||||
print(f"{elapsed}ms")
|
||||
|
||||
# print(Color.downgrade.cache_info())
|
||||
# print(Color.parse.cache_info())
|
||||
# print(Color.get_ansi_codes.cache_info())
|
||||
|
||||
# print(Style.parse.cache_info())
|
||||
|
||||
|
|
|
@ -24,8 +24,11 @@ if TYPE_CHECKING:
|
|||
RenderableType,
|
||||
RenderResult,
|
||||
)
|
||||
from .containers import Lines
|
||||
|
||||
from . import errors
|
||||
from .padding import Padding, PaddingDimensions
|
||||
from .protocol import is_renderable
|
||||
from .segment import Segment
|
||||
from .style import Style
|
||||
from .text import Text
|
||||
|
@ -37,8 +40,8 @@ from ._tools import iter_first_last, iter_first, iter_last, ratio_divide
|
|||
class Column:
|
||||
"""Defines a column in a table."""
|
||||
|
||||
header: Union[str, "ConsoleRenderable"] = ""
|
||||
footer: Union[str, "ConsoleRenderable"] = ""
|
||||
header: "RenderableType" = ""
|
||||
footer: "RenderableType" = ""
|
||||
header_style: Union[str, Style] = "table.header"
|
||||
footer_style: Union[str, Style] = "table.footer"
|
||||
style: Union[str, Style] = "none"
|
||||
|
@ -46,10 +49,10 @@ class Column:
|
|||
width: Optional[int] = None
|
||||
ratio: Optional[int] = None
|
||||
no_wrap: bool = False
|
||||
_cells: List["ConsoleRenderable"] = field(default_factory=list)
|
||||
_cells: List["RenderableType"] = field(default_factory=list)
|
||||
|
||||
@property
|
||||
def cells(self) -> Iterable["ConsoleRenderable"]:
|
||||
def cells(self) -> Iterable["RenderableType"]:
|
||||
"""Get all cells in the column, not including header."""
|
||||
yield from self._cells
|
||||
|
||||
|
@ -58,27 +61,11 @@ class Column:
|
|||
"""Check if this column is flexible."""
|
||||
return self.ratio is not None
|
||||
|
||||
@property
|
||||
def header_renderable(self) -> "ConsoleRenderable":
|
||||
return (
|
||||
Text.from_markup(self.header, style=self.header_style or "")
|
||||
if isinstance(self.header, str)
|
||||
else self.header
|
||||
)
|
||||
|
||||
@property
|
||||
def footer_renderable(self) -> "ConsoleRenderable":
|
||||
return (
|
||||
Text.from_markup(self.footer, style=self.footer_style or "")
|
||||
if isinstance(self.footer, str)
|
||||
else self.footer
|
||||
)
|
||||
|
||||
|
||||
class _Cell(NamedTuple):
|
||||
|
||||
style: Union[str, Style]
|
||||
renderable: "ConsoleRenderable"
|
||||
renderable: "RenderableType"
|
||||
|
||||
|
||||
class Table:
|
||||
|
@ -202,7 +189,7 @@ class Table:
|
|||
)
|
||||
self.columns.append(column)
|
||||
|
||||
def add_row(self, *renderables: Optional[Union[str, "ConsoleRenderable"]]) -> None:
|
||||
def add_row(self, *renderables: Optional["RenderableType"]) -> None:
|
||||
"""Add a row of renderables.
|
||||
|
||||
Raises:
|
||||
|
@ -210,12 +197,10 @@ class Table:
|
|||
"""
|
||||
from .console import ConsoleRenderable
|
||||
|
||||
def add_cell(column: Column, renderable: ConsoleRenderable):
|
||||
def add_cell(column: Column, renderable: "RenderableType") -> None:
|
||||
column._cells.append(renderable)
|
||||
|
||||
cell_renderables: List[Optional[Union[str, ConsoleRenderable]]] = list(
|
||||
renderables
|
||||
)
|
||||
cell_renderables: List[Optional["RenderableType"]] = list(renderables)
|
||||
|
||||
columns = self.columns
|
||||
if len(cell_renderables) < len(columns):
|
||||
|
@ -231,12 +216,10 @@ class Table:
|
|||
self.columns.append(column)
|
||||
else:
|
||||
column = columns[index]
|
||||
if isinstance(renderable, ConsoleRenderable):
|
||||
if renderable is None:
|
||||
add_cell(column, "")
|
||||
elif is_renderable(renderable):
|
||||
add_cell(column, renderable)
|
||||
elif renderable is None:
|
||||
add_cell(column, Text(""))
|
||||
elif isinstance(renderable, str):
|
||||
add_cell(column, Text.from_markup(renderable))
|
||||
else:
|
||||
raise errors.NotRenderableError(
|
||||
f"unable to render {renderable!r}; str or object with a __console__ method is required"
|
||||
|
@ -257,32 +240,24 @@ class Table:
|
|||
max_width -= 2
|
||||
widths = self._calculate_column_widths(max_width)
|
||||
table_width = sum(widths) + len(self.columns) + (2 if self.box else 0)
|
||||
yield from self.render_title(table_width)
|
||||
|
||||
def render_annotation(
|
||||
text: Union[Text, str], style: Union[str, Style]
|
||||
) -> "Lines":
|
||||
if isinstance(text, Text):
|
||||
render_text = text
|
||||
else:
|
||||
render_text = console.render_str(text, style=style)
|
||||
return render_text.wrap(table_width, "center")
|
||||
|
||||
yield render_annotation(
|
||||
self.title or "", style=Style.pick_first(self.title_style, "table.title")
|
||||
)
|
||||
yield from self._render(console, options, widths)
|
||||
yield from self.render_caption(table_width)
|
||||
|
||||
def render_title(self, table_width: int) -> "RenderResult":
|
||||
"""Render the title. Override if you want to render the title differently."""
|
||||
if self.title:
|
||||
if isinstance(self.title, str):
|
||||
title_text = Text.from_markup(
|
||||
self.title, style=Style.pick_first(self.title_style, "table.title")
|
||||
)
|
||||
else:
|
||||
title_text = self.title
|
||||
yield title_text.wrap(table_width, "center")
|
||||
|
||||
def render_caption(self, table_width: int) -> "RenderResult":
|
||||
"""Render the caption. Override if you want to render the caption differently."""
|
||||
if self.caption:
|
||||
if isinstance(self.caption, str):
|
||||
caption_text = Text.from_markup(
|
||||
self.caption,
|
||||
style=Style.pick_first(self.caption_style, "table.caption"),
|
||||
)
|
||||
else:
|
||||
caption_text = self.caption
|
||||
yield caption_text.wrap(table_width, "center")
|
||||
yield render_annotation(
|
||||
self.caption or "",
|
||||
style=Style.pick_first(self.caption_style, "table.caption"),
|
||||
)
|
||||
|
||||
def _calculate_column_widths(self, max_width: int) -> List[int]:
|
||||
"""Calculate the widths of each column."""
|
||||
|
@ -342,7 +317,7 @@ class Table:
|
|||
first = column_index == 0
|
||||
last = column_index == len(self.columns) - 1
|
||||
|
||||
def add_padding(renderable: "ConsoleRenderable") -> "ConsoleRenderable":
|
||||
def add_padding(renderable: "RenderableType") -> "RenderableType":
|
||||
if not any_padding:
|
||||
return renderable
|
||||
top, right, bottom, left = padding
|
||||
|
@ -354,11 +329,11 @@ class Table:
|
|||
return Padding(renderable, (top, right, bottom, left))
|
||||
|
||||
if self.show_header:
|
||||
yield _Cell(column.header_style, add_padding(column.header_renderable))
|
||||
yield _Cell(column.header_style, add_padding(column.header))
|
||||
for cell in column.cells:
|
||||
yield _Cell(column.style, add_padding(cell))
|
||||
if self.show_footer:
|
||||
yield _Cell(column.footer_style, add_padding(column.footer_renderable))
|
||||
yield _Cell(column.footer_style, add_padding(column.footer))
|
||||
|
||||
def _measure_column(
|
||||
self, column_index: int, column: Column, max_width: int
|
||||
|
@ -478,7 +453,7 @@ if __name__ == "__main__": # pragma: no cover
|
|||
from .console import Console
|
||||
from . import box
|
||||
|
||||
c = Console()
|
||||
c = Console(markup=False)
|
||||
table = Table(
|
||||
Column(
|
||||
"Foo", footer=Text("Total", justify="right"), footer_style="bold", ratio=1
|
||||
|
@ -494,7 +469,7 @@ if __name__ == "__main__": # pragma: no cover
|
|||
# table.columns[0].width = 50
|
||||
# table.columns[1].ratio = 1
|
||||
|
||||
table.add_row("Hello, World! " * 3, "cake" * 10)
|
||||
table.add_row("Hello, [b]World[/b]! " * 3, "cake" * 10)
|
||||
from .markdown import Markdown
|
||||
|
||||
table.add_row(Markdown("# This is *Markdown*!"), "More text", "Hello WOrld")
|
||||
|
|
74
rich/text.py
74
rich/text.py
|
@ -2,6 +2,7 @@ from operator import itemgetter
|
|||
import re
|
||||
from typing import (
|
||||
Any,
|
||||
cast,
|
||||
Dict,
|
||||
Iterable,
|
||||
NamedTuple,
|
||||
|
@ -91,6 +92,7 @@ class Text:
|
|||
style (Union[str, Style], optional): Base style for text. Defaults to "".
|
||||
justify (str, optional): Default alignment for text, "left", "center", "full" or "right". Defaults to None.
|
||||
end (str, optional): Character to end text with. Defaults to "\n".
|
||||
tab_size (int, optional): Number of spaces per tab, or ``None`` to use ``console.tab_size``. Defaults to None.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
|
@ -99,12 +101,13 @@ class Text:
|
|||
style: Union[str, Style] = "",
|
||||
justify: "JustifyValues" = None,
|
||||
end: str = "\n",
|
||||
tab_size: int = None,
|
||||
) -> None:
|
||||
|
||||
self._text: List[str] = [text] if text else []
|
||||
self.style = style
|
||||
self.justify = justify
|
||||
self.end = end
|
||||
self.tab_size = tab_size
|
||||
self._spans: List[Span] = []
|
||||
self._length: int = len(text)
|
||||
|
||||
|
@ -132,6 +135,13 @@ class Text:
|
|||
return NotImplemented
|
||||
return self.text == other.text and self._spans == other._spans
|
||||
|
||||
def __contains__(self, other: object) -> bool:
|
||||
if isinstance(other, str):
|
||||
return other in self.text
|
||||
elif isinstance(other, Text):
|
||||
return other.text in self.text
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def from_markup(cls, text: str, style: Union[str, Style] = "") -> "Text":
|
||||
"""Create Text instance from markup.
|
||||
|
@ -153,8 +163,9 @@ class Text:
|
|||
style: Union[str, Style] = "",
|
||||
justify: "JustifyValues" = None,
|
||||
end: str = "\n",
|
||||
tab_size: Optional[int] = None,
|
||||
) -> "Text":
|
||||
text = cls(style=style, justify=justify, end=end)
|
||||
text = cls(style=style, justify=justify, end=end, tab_size=tab_size)
|
||||
append = text.append
|
||||
for part in parts:
|
||||
append(*part)
|
||||
|
@ -182,7 +193,11 @@ class Text:
|
|||
"""Return a copy of this instance."""
|
||||
self.text
|
||||
copy_self = Text(
|
||||
self.text, style=self.style, justify=self.justify, end=self.end,
|
||||
self.text,
|
||||
style=self.style,
|
||||
justify=self.justify,
|
||||
end=self.end,
|
||||
tab_size=self.tab_size,
|
||||
)
|
||||
copy_self._spans[:] = self._spans[:]
|
||||
return copy_self
|
||||
|
@ -281,7 +296,21 @@ class Text:
|
|||
def __console__(
|
||||
self, console: "Console", options: "ConsoleOptions"
|
||||
) -> Iterable[Segment]:
|
||||
lines = self.wrap(options.max_width, justify=self.justify or options.justify)
|
||||
# TODO: Why does mypy give "error: Cannot determine type of 'tab_size'"" ?
|
||||
if self.tab_size is None:
|
||||
tab_size = console.tab_size # type: ignore
|
||||
else:
|
||||
tab_size = self.tab_size
|
||||
|
||||
# if self.tab_size is None:
|
||||
# tab_size = console.tab_size
|
||||
# else:
|
||||
# tab_size = self.tab_size
|
||||
lines = self.wrap(
|
||||
options.max_width,
|
||||
justify=self.justify or options.justify,
|
||||
tab_size=tab_size,
|
||||
)
|
||||
all_lines = Text("\n").join(lines)
|
||||
yield from self._render_line(all_lines, console, options)
|
||||
|
||||
|
@ -373,6 +402,37 @@ class Text:
|
|||
append(self)
|
||||
return new_text
|
||||
|
||||
def tabs_to_spaces(self, tab_size: int = 8) -> "Text":
|
||||
"""Get a new string with tabs converted to spaces.
|
||||
|
||||
Args:
|
||||
tab_size (int, optional): Size of tabs. Defaults to 8.
|
||||
|
||||
Returns:
|
||||
Text: A new instance with tabs replaces by spaces.
|
||||
"""
|
||||
if "\t" not in self.text:
|
||||
return self.copy()
|
||||
parts = self.split("\t", include_separator=True)
|
||||
pos = 0
|
||||
result = Text(
|
||||
style=self.style, justify=self.justify, end=self.end, tab_size=self.tab_size
|
||||
)
|
||||
append = result.append
|
||||
|
||||
for part in parts:
|
||||
if part.text.endswith("\t"):
|
||||
part._text = [part.text[:-1] + " "]
|
||||
append(part)
|
||||
pos += len(part)
|
||||
spaces = tab_size - ((pos - 1) % tab_size) - 1
|
||||
if spaces:
|
||||
append(" " * spaces, self.style)
|
||||
pos += spaces
|
||||
else:
|
||||
append(part)
|
||||
return result
|
||||
|
||||
def _trim_spans(self) -> None:
|
||||
"""Remove or modify any spans that are over the end of the text."""
|
||||
new_length = self._length
|
||||
|
@ -537,7 +597,9 @@ class Text:
|
|||
"""Remove a number of characters from the end of the text."""
|
||||
self.text = self.text[:-amount]
|
||||
|
||||
def wrap(self, width: int, justify: "JustifyValues" = "left") -> Lines:
|
||||
def wrap(
|
||||
self, width: int, justify: "JustifyValues" = "left", tab_size: int = 8
|
||||
) -> Lines:
|
||||
"""Word wrap the text.
|
||||
|
||||
Args:
|
||||
|
@ -550,6 +612,8 @@ class Text:
|
|||
|
||||
lines: Lines = Lines()
|
||||
for line in self.split():
|
||||
if "\t" in line:
|
||||
line = line.tabs_to_spaces(tab_size)
|
||||
offsets = divide_line(str(line), width)
|
||||
new_lines = line.divide(offsets)
|
||||
if justify:
|
||||
|
|
Loading…
Reference in New Issue