mirror of https://github.com/Textualize/rich.git
text formatting
This commit is contained in:
parent
8708f78689
commit
724d3d8ae1
|
@ -3,7 +3,7 @@ from __future__ import annotations
|
|||
|
||||
from collections import ChainMap
|
||||
from contextlib import contextmanager
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import dataclass, replace
|
||||
from enum import Enum
|
||||
import re
|
||||
import shutil
|
||||
|
@ -37,6 +37,9 @@ class ConsoleOptions:
|
|||
encoding: str
|
||||
min_width: int = 1
|
||||
|
||||
def copy(self) -> ConsoleOptions:
|
||||
return replace(self)
|
||||
|
||||
|
||||
class SupportsStr(Protocol):
|
||||
def __str__(self) -> str:
|
||||
|
@ -92,7 +95,7 @@ class Console:
|
|||
file: IO = None,
|
||||
width: int = None,
|
||||
height: int = None,
|
||||
markup: str = "markdown",
|
||||
markup: Optional[str] = "markdown",
|
||||
):
|
||||
self._styles = ChainMap(styles)
|
||||
self.file = file or sys.stdout
|
||||
|
@ -167,7 +170,7 @@ class Console:
|
|||
self._check_buffer()
|
||||
|
||||
def render(
|
||||
self, renderable: RenderableType, options: ConsoleOptions
|
||||
self, renderable: RenderableType, options: Optional[ConsoleOptions]
|
||||
) -> Iterable[StyledText]:
|
||||
"""Render an object in to an iterable of `StyledText` instances.
|
||||
|
||||
|
@ -184,18 +187,46 @@ class Console:
|
|||
Iterable[StyledText]: An iterable of styled text that may be rendered.
|
||||
"""
|
||||
render_iterable: Iterable[RenderableType]
|
||||
render_options = options or self.options
|
||||
if isinstance(renderable, StyledText):
|
||||
yield renderable
|
||||
elif isinstance(renderable, ConsoleRenderable):
|
||||
render_iterable = renderable.__console__(self, options)
|
||||
render_iterable = renderable.__console__(self, render_options)
|
||||
else:
|
||||
render_iterable = self.render_str(str(renderable), options)
|
||||
render_iterable = self.render_str(str(renderable), render_options)
|
||||
|
||||
for render_output in render_iterable:
|
||||
if isinstance(render_output, StyledText):
|
||||
yield render_output
|
||||
else:
|
||||
yield from self.render(render_output, options)
|
||||
yield from self.render(render_output, render_options)
|
||||
|
||||
def render_all(
|
||||
self, renderables: Iterable[RenderableType], options: Optional[ConsoleOptions]
|
||||
) -> Iterable[StyledText]:
|
||||
render_options = options or self.options
|
||||
for renderable in renderables:
|
||||
yield from self.render(renderable, render_options)
|
||||
|
||||
def render_lines(
|
||||
self, renderables: Iterable[RenderableType], options: Optional[ConsoleOptions]
|
||||
) -> List[List[StyledText]]:
|
||||
from .text import Text
|
||||
|
||||
contents: List[StyledText] = []
|
||||
render_options = options or self.options
|
||||
for renderable in renderables:
|
||||
contents.extend(self.render(renderable, render_options))
|
||||
|
||||
new_text = Text.from_styled_text(contents)
|
||||
|
||||
split_text = new_text.split()
|
||||
lines: List[List[StyledText]] = []
|
||||
for line in split_text:
|
||||
line.end = ""
|
||||
line.set_length(render_options.max_width)
|
||||
lines.append(list(line.__console__(self, render_options)))
|
||||
return lines
|
||||
|
||||
def render_str(
|
||||
self, text: str, options: ConsoleOptions
|
||||
|
|
|
@ -46,13 +46,13 @@ MARKDOWN_STYLES = {
|
|||
"markdown.block_quote": Style("markdown.code_block", color="magenta"),
|
||||
"markdown.list": Style("markdown.list", color="cyan"),
|
||||
"markdown.item": Style("markdown.item", underline=True),
|
||||
"markdown.hr": Style("markdown.hr"),
|
||||
"markdown.h1": Style("markdown.h1", bold=True, underline=True),
|
||||
"markdown.h2": Style("markdown.h2", bold=True),
|
||||
"markdown.h3": Style("markdown.h3", bold=True, dim=True),
|
||||
"markdown.h4": Style("markdown.h4", bold=True, italic=True),
|
||||
"markdown.h5": Style("markdown.h5", bold=True),
|
||||
"markdown.h6": Style("markdown.h6", bold=True),
|
||||
"markdown.h7": Style("markdown.h7", bold=True),
|
||||
"markdown.hr": Style("markdown.hr", dim=True),
|
||||
"markdown.h1": Style("markdown.h1", bold=True),
|
||||
"markdown.h2": Style("markdown.h2", bold=True, underline=True),
|
||||
"markdown.h3": Style("markdown.h3", bold=True),
|
||||
"markdown.h4": Style("markdown.h4", bold=True, dim=True),
|
||||
"markdown.h5": Style("markdown.h5", underline=True),
|
||||
"markdown.h6": Style("markdown.h6", italic=True),
|
||||
"markdown.h7": Style("markdown.h7", italic=True, dim=True),
|
||||
}
|
||||
DEFAULT_STYLES.update(MARKDOWN_STYLES)
|
||||
|
|
149
rich/markdown.py
149
rich/markdown.py
|
@ -1,7 +1,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, Iterable, List, Optional, Union
|
||||
from typing import Any, ClassVar, Dict, Iterable, List, Optional, Union
|
||||
|
||||
from commonmark.blocks import Parser
|
||||
|
||||
|
@ -12,12 +12,16 @@ from .console import (
|
|||
RenderResult,
|
||||
StyledText,
|
||||
)
|
||||
from .panel import Panel
|
||||
from .style import Style, StyleStack
|
||||
from .text import Lines, Text
|
||||
from ._stack import Stack
|
||||
|
||||
|
||||
class MarkdownElement:
|
||||
|
||||
new_line: ClassVar[bool] = True
|
||||
|
||||
@classmethod
|
||||
def create(cls, node: Any) -> MarkdownElement:
|
||||
return cls()
|
||||
|
@ -32,8 +36,8 @@ class MarkdownElement:
|
|||
return
|
||||
yield
|
||||
|
||||
def on_child_close(self, context: MarkdownContext, child: MarkdownElement) -> None:
|
||||
pass
|
||||
def on_child_close(self, context: MarkdownContext, child: MarkdownElement) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
class UnknownElement(MarkdownElement):
|
||||
|
@ -44,22 +48,17 @@ class TextElement(MarkdownElement):
|
|||
|
||||
style_name = "none"
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.text = Text()
|
||||
|
||||
def on_enter(self, context: MarkdownContext) -> None:
|
||||
context.enter_style(self.style_name)
|
||||
self.text = Text(style=context.current_style)
|
||||
|
||||
def on_text(self, context: MarkdownContext, text: str) -> None:
|
||||
self.text.append(text, context.current_style)
|
||||
|
||||
def on_leave(self, context: MarkdownContext) -> Iterable[Lines]:
|
||||
def on_leave(self, context: MarkdownContext) -> RenderResult:
|
||||
context.leave_style()
|
||||
yield self.text
|
||||
|
||||
def on_child_close(self, context: MarkdownContext, child: MarkdownElement) -> None:
|
||||
pass
|
||||
|
||||
|
||||
class Paragraph(TextElement):
|
||||
style_name = "markdown.paragraph"
|
||||
|
@ -67,7 +66,7 @@ class Paragraph(TextElement):
|
|||
def on_leave(self, context: MarkdownContext) -> Iterable[Lines]:
|
||||
context.leave_style()
|
||||
lines = self.text.wrap(context.options.max_width)
|
||||
yield lines
|
||||
yield from lines
|
||||
|
||||
|
||||
class Heading(TextElement):
|
||||
|
@ -76,16 +75,63 @@ class Heading(TextElement):
|
|||
heading = Heading(node.level)
|
||||
return heading
|
||||
|
||||
def on_enter(self, context: MarkdownContext) -> None:
|
||||
self.text = Text(style=context.current_style, end="")
|
||||
context.enter_style(self.style_name)
|
||||
|
||||
def __init__(self, level: int) -> None:
|
||||
self.level = level
|
||||
self.style_name = f"markdown.h{level}"
|
||||
super().__init__()
|
||||
|
||||
def on_leave(self, context: MarkdownContext) -> Iterable[Lines]:
|
||||
context.leave_style()
|
||||
lines = self.text.wrap(context.options.max_width, justify="center")
|
||||
self.text.justify = "center"
|
||||
if self.level == 1:
|
||||
yield Panel(self.text)
|
||||
else:
|
||||
yield self.text
|
||||
yield StyledText("\n")
|
||||
# lines = self.text.wrap(context.options.max_width, justify="center")
|
||||
# yield lines
|
||||
|
||||
|
||||
class CodeBlock(TextElement):
|
||||
style_name = "markdown.code_block"
|
||||
|
||||
def on_leave(self, context: MarkdownContext) -> Iterable[Lines]:
|
||||
context.leave_style()
|
||||
lines = self.text.wrap(context.options.max_width, justify="left")
|
||||
yield lines
|
||||
|
||||
|
||||
class BlockQuote(TextElement):
|
||||
style_name = "markdown.block_quote"
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.elements = []
|
||||
|
||||
def on_enter(self, context: MarkdownContext) -> None:
|
||||
context.enter_style(self.style_name)
|
||||
|
||||
def on_child_close(self, context: MarkdownContext, child: MarkdownElement) -> bool:
|
||||
self.elements.append(child)
|
||||
return False
|
||||
|
||||
def on_leave(self, context: MarkdownContext) -> Iterable[Lines]:
|
||||
context.leave_style()
|
||||
for element in self.elements:
|
||||
yield from element.on_leave(context)
|
||||
|
||||
|
||||
class HorizontalRule(MarkdownElement):
|
||||
new_line = False
|
||||
|
||||
def on_leave(self, context: MarkdownContext) -> Iterable[StyledText]:
|
||||
style = context.console.get_style("markdown.hr")
|
||||
yield StyledText("─" * context.options.max_width, style)
|
||||
|
||||
|
||||
class MarkdownContext:
|
||||
"""Manages the console render state."""
|
||||
|
||||
|
@ -97,24 +143,34 @@ class MarkdownContext:
|
|||
|
||||
@property
|
||||
def current_style(self) -> Style:
|
||||
"""Current style which is the product of all styles on the stack."""
|
||||
return self.style_stack.current
|
||||
|
||||
def on_text(self, text: str) -> None:
|
||||
"""Called when the parser visits text."""
|
||||
self.stack.top.on_text(self, text)
|
||||
|
||||
def enter_style(self, style_name: str) -> None:
|
||||
style = console.get_style(style_name) or console.get_style("none")
|
||||
"""Enter a style context."""
|
||||
style = self.console.get_style(style_name) or self.console.get_style("none")
|
||||
self.style_stack.push(style)
|
||||
|
||||
def leave_style(self) -> Style:
|
||||
"""Leave a style context."""
|
||||
style = self.style_stack.pop()
|
||||
return style
|
||||
|
||||
|
||||
class Markdown:
|
||||
|
||||
elements = {"paragraph": Paragraph, "heading": Heading}
|
||||
inlines = {"emph", "strong"}
|
||||
elements: ClassVar[Dict[str, MarkdownElement]] = {
|
||||
"paragraph": Paragraph,
|
||||
"heading": Heading,
|
||||
"code_block": CodeBlock,
|
||||
"block_quote": BlockQuote,
|
||||
"thematic_break": HorizontalRule,
|
||||
}
|
||||
inlines = {"emph", "strong", "code"}
|
||||
|
||||
def __init__(self, markup: str) -> None:
|
||||
"""Parses the markup."""
|
||||
|
@ -126,36 +182,56 @@ class Markdown:
|
|||
"""Render markdown to the console."""
|
||||
context = MarkdownContext(console, options)
|
||||
nodes = self.parsed.walker()
|
||||
|
||||
inlines = self.inlines
|
||||
for current, entering in nodes:
|
||||
# print(dir(current))
|
||||
print(current, entering)
|
||||
print(current.is_container())
|
||||
node_type = current.t
|
||||
if node_type == "text":
|
||||
context.on_text(current.literal)
|
||||
elif node_type == "softbreak":
|
||||
if entering:
|
||||
context.on_text("\n")
|
||||
elif node_type in self.inlines:
|
||||
if entering:
|
||||
context.enter_style(f"markdown.{node_type}")
|
||||
elif node_type in inlines:
|
||||
if current.is_container():
|
||||
if entering:
|
||||
context.enter_style(f"markdown.{node_type}")
|
||||
else:
|
||||
context.leave_style()
|
||||
else:
|
||||
context.enter_style(f"markdown.{node_type}")
|
||||
if current.literal:
|
||||
context.on_text(current.literal)
|
||||
context.leave_style()
|
||||
else:
|
||||
element_class = self.elements.get(node_type) or UnknownElement
|
||||
if current.is_container():
|
||||
if entering:
|
||||
element = element_class.create(current)
|
||||
context.stack.push(element)
|
||||
element.on_enter(context)
|
||||
else:
|
||||
element = context.stack.pop()
|
||||
|
||||
if entering:
|
||||
if context.stack:
|
||||
if context.stack.top.on_child_close(context, element):
|
||||
if element.new_line:
|
||||
yield StyledText("\n")
|
||||
yield from element.on_leave(context)
|
||||
else:
|
||||
yield from element.on_leave(context)
|
||||
else:
|
||||
element = element_class.create(current)
|
||||
context.stack.push(element)
|
||||
element.on_enter(context)
|
||||
else:
|
||||
element = context.stack.pop()
|
||||
if context.stack:
|
||||
if current.literal:
|
||||
element.on_text(context, current.literal.rstrip())
|
||||
context.stack.pop()
|
||||
if context.stack and element.new_line:
|
||||
yield StyledText("\n")
|
||||
yield from element.on_leave(context)
|
||||
|
||||
if context.stack:
|
||||
context.stack.top.on_child_close(context, element)
|
||||
|
||||
|
||||
# class Markdown:
|
||||
# """Render markdown to the console."""
|
||||
|
@ -276,6 +352,16 @@ class Markdown:
|
|||
markup = """
|
||||
# This is a header
|
||||
|
||||
## This is a header L2
|
||||
|
||||
### This is a header L3
|
||||
|
||||
#### This is a header L4
|
||||
|
||||
##### This is a header L5
|
||||
|
||||
###### This is a header L6
|
||||
|
||||
The main area where I think *Django's models* are `missing` out is the lack of type hinting (hardly surprising since **Django** pre-dates type hints). Adding type hints allows Mypy to detect bugs before you even run your code. It may only save you minutes each time, but multiply that by the number of code + run iterations you do each day, and it can save hours of development time. Multiply that by the lifetime of your project, and it could save weeks or months. A clear win.
|
||||
|
||||
```
|
||||
|
@ -301,13 +387,14 @@ The main area where I think Django's models are missing out is the lack of type
|
|||
* baz
|
||||
"""
|
||||
|
||||
markup = """\
|
||||
# Heading
|
||||
# markup = """\
|
||||
# # Heading
|
||||
|
||||
Hello, *World*!
|
||||
**Bold**
|
||||
# This is `code`!
|
||||
# Hello, *World*!
|
||||
# **Bold**
|
||||
|
||||
"""
|
||||
# """
|
||||
|
||||
if __name__ == "__main__":
|
||||
from .console import Console
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import Tuple
|
||||
|
||||
from .console import Console, ConsoleOptions, ConsoleRenderable, RenderResult
|
||||
from .text import Text
|
||||
from .styled_text import StyledText
|
||||
|
||||
BOX = """\
|
||||
┌─┐
|
||||
│ │
|
||||
└─┘
|
||||
"""
|
||||
|
||||
|
||||
class Panel:
|
||||
def __init__(self, *contents: ConsoleRenderable) -> None:
|
||||
self.contents: Tuple[ConsoleRenderable, ...] = contents
|
||||
|
||||
def __console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
|
||||
|
||||
width = options.max_width
|
||||
contents_width = width - 2
|
||||
|
||||
child_options = options.copy()
|
||||
child_options.max_width = contents_width
|
||||
|
||||
lines = console.render_lines(self.contents, child_options)
|
||||
# contents = list(console.render_all(self.contents, child_options))
|
||||
# lines = Text.reformat(contents_width, contents)
|
||||
|
||||
box = BOX
|
||||
top_left = box[0]
|
||||
top = box[1]
|
||||
top_right = box[2]
|
||||
left = box[4]
|
||||
right = box[6]
|
||||
bottom_left = box[8]
|
||||
bottom = box[9]
|
||||
bottom_right = box[10]
|
||||
|
||||
line_start = StyledText(left)
|
||||
line_end = StyledText(f"{right}\n")
|
||||
|
||||
yield StyledText(f"{top_left}{top * contents_width}{top_right}\n")
|
||||
for line in lines:
|
||||
yield line_start
|
||||
yield from line
|
||||
yield line_end
|
||||
yield StyledText(f"{bottom_left}{bottom * contents_width}{bottom_right}\n")
|
|
@ -117,6 +117,14 @@ class Style:
|
|||
else:
|
||||
return f'<style {self.name} "{self}">'
|
||||
|
||||
def __eq__(self, other) -> bool:
|
||||
return (
|
||||
self._color == other._color
|
||||
and self._back == self._back
|
||||
and self._set_attributes == other._set_attributes
|
||||
and self._attributes == other._attributes,
|
||||
)
|
||||
|
||||
@property
|
||||
def color(self) -> Optional[str]:
|
||||
return self._color.name if self._color is not None else None
|
||||
|
@ -281,6 +289,7 @@ class Style:
|
|||
return self
|
||||
|
||||
new_style = style.__new__(Style)
|
||||
new_style.name = self.name
|
||||
new_style._color = self._color if style._color is None else style._color
|
||||
new_style._back = self._back if style._back is None else style._back
|
||||
new_style._attributes = (style._attributes & ~self._set_attributes) | (
|
||||
|
|
141
rich/text.py
141
rich/text.py
|
@ -8,6 +8,8 @@ from .console import Console, ConsoleOptions, RenderResult, RenderableType
|
|||
from .style import Style
|
||||
from .styled_text import StyledText
|
||||
|
||||
JustifyValues = Optional[Literal["left", "center", "right"]]
|
||||
|
||||
|
||||
class TextSpan(NamedTuple):
|
||||
"""A marked up region in some text."""
|
||||
|
@ -19,7 +21,7 @@ class TextSpan(NamedTuple):
|
|||
def __repr__(self) -> str:
|
||||
return f'<textspan {self.start}:{self.end} "{self.style}">'
|
||||
|
||||
def split(self, offset: int) -> Tuple[Optional[TextSpan], Optional[TextSpan]]:
|
||||
def split(self, offset: int) -> Tuple[TextSpan, Optional[TextSpan]]:
|
||||
"""Split a span in to 2 from a given offset."""
|
||||
|
||||
if offset < self.start:
|
||||
|
@ -87,19 +89,25 @@ class TextSpan(NamedTuple):
|
|||
|
||||
def right_crop(self, offset: int) -> TextSpan:
|
||||
start, end, style = self
|
||||
if offset > end:
|
||||
if offset >= end:
|
||||
return self
|
||||
return TextSpan(start, max(offset, end), style)
|
||||
return TextSpan(start, min(offset, end), style)
|
||||
|
||||
|
||||
class Text:
|
||||
"""Text with colored spans."""
|
||||
|
||||
def __init__(
|
||||
self, text: str = "", style: Union[str, Style] = None, end: str = "",
|
||||
self,
|
||||
text: str = "",
|
||||
style: Union[str, Style] = None,
|
||||
justify: JustifyValues = "left",
|
||||
end: str = "\n",
|
||||
) -> None:
|
||||
self._text: List[str] = [text]
|
||||
self._style = style
|
||||
self._text: List[str] = [text] if text else []
|
||||
self.style = style
|
||||
self.justify = justify
|
||||
self.end = end
|
||||
self._text_str: Optional[str] = text
|
||||
self._spans: List[TextSpan] = []
|
||||
self._length: int = len(text)
|
||||
|
@ -113,9 +121,24 @@ class Text:
|
|||
def __repr__(self) -> str:
|
||||
return f"<text {self.text!r} {self._spans!r}>"
|
||||
|
||||
@classmethod
|
||||
def from_styled_text(cls, styled_text: Iterable[StyledText]) -> Text:
|
||||
text = Text(justify=None)
|
||||
append_span = text._spans.append
|
||||
append_text = text._text.append
|
||||
offset = 0
|
||||
for text_str, style in styled_text:
|
||||
span_length = len(text_str)
|
||||
append_span(TextSpan(offset, offset + span_length, style or "none"))
|
||||
append_text(text_str)
|
||||
offset += span_length
|
||||
text._length += span_length
|
||||
text._text_str = None
|
||||
return text
|
||||
|
||||
def copy(self) -> Text:
|
||||
"""Return a copy of this instance."""
|
||||
copy_self = Text(self.text, style=self._style)
|
||||
copy_self = Text(self.text, style=self.style)
|
||||
copy_self._spans = self._spans[:]
|
||||
return copy_self
|
||||
|
||||
|
@ -136,8 +159,32 @@ class Text:
|
|||
return
|
||||
self._spans.append(TextSpan(max(0, start), min(length, end), style))
|
||||
|
||||
def set_length(self, new_length: int) -> None:
|
||||
"""Set new length of the text, clipping or padding is required."""
|
||||
length = len(self)
|
||||
if length == new_length:
|
||||
return
|
||||
if length < new_length:
|
||||
self.pad_right(new_length - length)
|
||||
else:
|
||||
text = self.text[:new_length]
|
||||
self.text = text
|
||||
new_spans = []
|
||||
for span in self._spans:
|
||||
if span.start >= new_length:
|
||||
break
|
||||
new_spans.append(span.right_crop(new_length))
|
||||
self._spans[:] = new_spans
|
||||
|
||||
def __console__(
|
||||
self, console: Console, options: ConsoleOptions
|
||||
) -> Iterable[StyledText]:
|
||||
lines = self.wrap(options.max_width, justify=self.justify)
|
||||
for line in lines:
|
||||
yield from self._render_line(line, console, options)
|
||||
|
||||
def _render_line(
|
||||
self, line: Text, console: Console, options: ConsoleOptions
|
||||
) -> Iterable[StyledText]:
|
||||
"""Render the rich text to the console.
|
||||
|
||||
|
@ -149,26 +196,26 @@ class Text:
|
|||
Iterable[StyledText]: An iterable of styled text.
|
||||
"""
|
||||
|
||||
text = self.text
|
||||
text = line.text
|
||||
stack: List[Style] = []
|
||||
|
||||
null_style = Style()
|
||||
|
||||
def get_style(style: Union[str, Style]) -> Style:
|
||||
|
||||
if isinstance(style, str):
|
||||
return console.parse_style(style)
|
||||
return style
|
||||
|
||||
stack.append(get_style(self._style) if self._style is not None else Style())
|
||||
stack.append(get_style(line.style) if line.style is not None else Style())
|
||||
|
||||
start_spans = (
|
||||
(span.start, True, get_style(span.style) or null_style)
|
||||
for span in self._spans
|
||||
for span in line._spans
|
||||
)
|
||||
|
||||
end_spans = (
|
||||
(span.end, False, get_style(span.style) or null_style)
|
||||
for span in self._spans
|
||||
for span in line._spans
|
||||
)
|
||||
|
||||
spans = [
|
||||
|
@ -177,11 +224,13 @@ class Text:
|
|||
*end_spans,
|
||||
(len(text), False, null_style),
|
||||
]
|
||||
spans.sort(key=itemgetter(0))
|
||||
spans.sort(key=itemgetter(0, 1))
|
||||
|
||||
current_style = stack[-1]
|
||||
|
||||
for (offset, entering, _style), (next_offset, _, _) in zip(spans, spans[1:]):
|
||||
style = get_style(_style)
|
||||
|
||||
if entering:
|
||||
stack.append(style)
|
||||
current_style = current_style.apply(style)
|
||||
|
@ -190,12 +239,26 @@ class Text:
|
|||
stack.remove(style)
|
||||
stack.reverse()
|
||||
current_style = Style.combine(stack)
|
||||
span_text = text[offset:next_offset]
|
||||
yield StyledText(span_text, current_style)
|
||||
if next_offset > offset:
|
||||
span_text = text[offset:next_offset]
|
||||
yield StyledText(span_text, current_style)
|
||||
|
||||
while stack:
|
||||
style = stack.pop()
|
||||
yield StyledText("", style)
|
||||
# while stack:
|
||||
# style = stack.pop()
|
||||
# yield StyledText("", style)
|
||||
if self.end:
|
||||
yield StyledText(self.end)
|
||||
|
||||
@classmethod
|
||||
def join(cls, lines: Iterable[Text]) -> Text:
|
||||
"""Join lines in to a new text instance."""
|
||||
new_text = Text()
|
||||
new_text.text = "\n".join(line.text for line in lines)
|
||||
offset = 0
|
||||
for line in lines:
|
||||
new_text._spans.extend(span.move(offset) for span in line._spans)
|
||||
offset += len(line.text)
|
||||
return new_text
|
||||
|
||||
@property
|
||||
def text(self) -> str:
|
||||
|
@ -209,6 +272,7 @@ class Text:
|
|||
"""Set the text to a new value."""
|
||||
self._text[:] = [new_text]
|
||||
self._text_str = new_text
|
||||
self._length = len(new_text)
|
||||
return self
|
||||
|
||||
def pad_left(self, count: int, character: str = " ") -> None:
|
||||
|
@ -260,7 +324,7 @@ class Text:
|
|||
"""
|
||||
assert separator, "separator must not be empty"
|
||||
|
||||
text = self.text
|
||||
text = self.text.rstrip("\n")
|
||||
if separator not in text:
|
||||
return [self.copy()]
|
||||
offsets: List[int] = []
|
||||
|
@ -285,7 +349,8 @@ class Text:
|
|||
"""
|
||||
|
||||
if not offsets:
|
||||
return Lines([self.copy()])
|
||||
line = self.copy()
|
||||
return Lines([line])
|
||||
|
||||
text = self.text
|
||||
text_length = len(text)
|
||||
|
@ -294,7 +359,7 @@ class Text:
|
|||
average_line_length = -(-text_length // len(line_ranges))
|
||||
|
||||
new_lines = Lines(
|
||||
Text(text[start:end].rstrip(), style=self._style)
|
||||
Text(text[start:end].rstrip(), style=self.style)
|
||||
for start, end in line_ranges
|
||||
)
|
||||
|
||||
|
@ -325,9 +390,7 @@ class Text:
|
|||
|
||||
return new_lines
|
||||
|
||||
def wrap(
|
||||
self, width: int, justify: Literal["left", "center", "right"] = "left"
|
||||
) -> Lines:
|
||||
def wrap(self, width: int, justify: JustifyValues = "left") -> Lines:
|
||||
"""Word wrap the text.
|
||||
|
||||
Args:
|
||||
|
@ -366,7 +429,6 @@ class Lines(List[Text]):
|
|||
"""Console render method to insert line-breaks."""
|
||||
for line in self:
|
||||
yield line
|
||||
yield StyledText("\n")
|
||||
|
||||
def justify(
|
||||
self, width: int, align: Literal["left", "center", "right"] = "left"
|
||||
|
@ -393,26 +455,31 @@ if __name__ == "__main__":
|
|||
text = """\
|
||||
The main area where I think Django's models are missing out is the lack of type hinting (hardly surprising since Django pre-dates type hints). Adding type hints allows Mypy to detect bugs before you even run your code. It may only save you minutes each time, but multiply that by the number of code + run iterations you do each day, and it can save hours of development time. Multiply that by the lifetime of your project, and it could save weeks or months. A clear win.
|
||||
""".rstrip()
|
||||
console = Console()
|
||||
console = Console(width=50, markup=None)
|
||||
rtext = Text(text, style=Style.parse("on black"))
|
||||
rtext.stylize(20, 60, "bold yellow")
|
||||
rtext.stylize(28, 36, "underline")
|
||||
rtext.stylize(259, 357, "yellow on blue")
|
||||
print("\n")
|
||||
|
||||
console.print(rtext)
|
||||
print("\n")
|
||||
print(repr(rtext))
|
||||
print("\n")
|
||||
|
||||
# console.print(rtext)
|
||||
# lines = rtext.wrap(console.width, justify="left")
|
||||
# for line in lines:
|
||||
# print(repr(line))
|
||||
# print("-" * 50)
|
||||
|
||||
lines = rtext.wrap(50, justify="left")
|
||||
for line in lines:
|
||||
print(repr(line))
|
||||
print("-" * 50)
|
||||
# with console.style(Style()):
|
||||
# console.print(lines)
|
||||
|
||||
from .panel import Panel
|
||||
|
||||
print("--")
|
||||
panel = Panel(rtext)
|
||||
console.print(panel)
|
||||
|
||||
p = Text("hello")
|
||||
console.print(Panel(p))
|
||||
|
||||
with console.style(Style()):
|
||||
console.print(lines)
|
||||
# console.wrap(50)
|
||||
|
||||
# if __name__ == "__main__":
|
||||
|
|
Loading…
Reference in New Issue