mirror of https://github.com/Textualize/rich.git
Merge remote
This commit is contained in:
commit
a8a2e1d4b3
|
@ -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).
|
||||
|
||||
## [6.2.0] - 2020-09-13
|
||||
|
||||
### Added
|
||||
|
||||
- Added inline code highlighting to Markdown
|
||||
|
||||
## [6.1.2] - 2020-09-11
|
||||
|
||||
### Added
|
||||
|
|
|
@ -2,9 +2,11 @@ from rich import print
|
|||
from rich.console import render_group
|
||||
from rich.panel import Panel
|
||||
|
||||
|
||||
@render_group()
|
||||
def get_panels():
|
||||
yield Panel("Hello", style="on blue")
|
||||
yield Panel("World", style="on red")
|
||||
|
||||
|
||||
print(Panel(get_panels()))
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
from rich import print
|
||||
from rich.padding import Padding
|
||||
|
||||
test = Padding("Hello", (2, 4), style="on blue", expand=False)
|
||||
print(test)
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
name = "rich"
|
||||
homepage = "https://github.com/willmcgugan/rich"
|
||||
documentation = "https://rich.readthedocs.io/en/latest/"
|
||||
version = "6.1.2"
|
||||
version = "6.2.0"
|
||||
description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
|
||||
authors = ["Will McGugan <willmcgugan@gmail.com>"]
|
||||
license = "MIT"
|
||||
|
|
|
@ -182,7 +182,7 @@ class RenderGroup:
|
|||
|
||||
def render_group(fit: bool = False) -> Callable:
|
||||
"""A decorator that turns an iterable of renderables in to a group."""
|
||||
|
||||
|
||||
def decorator(method):
|
||||
"""Convert a method that returns an iterable of renderables in to a RenderGroup."""
|
||||
|
||||
|
|
|
@ -5,20 +5,14 @@ from commonmark.blocks import Parser
|
|||
from . import box
|
||||
from ._loop import loop_first
|
||||
from ._stack import Stack
|
||||
from .console import (
|
||||
Console,
|
||||
ConsoleOptions,
|
||||
JustifyMethod,
|
||||
RenderResult,
|
||||
Segment,
|
||||
)
|
||||
from .console import Console, ConsoleOptions, JustifyMethod, RenderResult, Segment
|
||||
from .containers import Renderables
|
||||
from .jupyter import JupyterMixin
|
||||
from .panel import Panel
|
||||
from .rule import Rule
|
||||
from .style import Style, StyleStack
|
||||
from .syntax import Syntax
|
||||
from .text import Text
|
||||
from .text import Text, TextType
|
||||
|
||||
|
||||
class MarkdownElement:
|
||||
|
@ -45,7 +39,7 @@ class MarkdownElement:
|
|||
context (MarkdownContext): The markdown context.
|
||||
"""
|
||||
|
||||
def on_text(self, context: "MarkdownContext", text: str) -> None:
|
||||
def on_text(self, context: "MarkdownContext", text: TextType) -> None:
|
||||
"""Called when text is parsed.
|
||||
|
||||
Args:
|
||||
|
@ -99,8 +93,8 @@ class TextElement(MarkdownElement):
|
|||
self.style = context.enter_style(self.style_name)
|
||||
self.text = Text(justify="left")
|
||||
|
||||
def on_text(self, context: "MarkdownContext", text: str) -> None:
|
||||
self.text.append(text, context.current_style)
|
||||
def on_text(self, context: "MarkdownContext", text: TextType) -> None:
|
||||
self.text.append(text, context.current_style if isinstance(text, str) else None)
|
||||
|
||||
def on_leave(self, context: "MarkdownContext") -> None:
|
||||
context.leave_style()
|
||||
|
@ -349,20 +343,38 @@ class ImageItem(TextElement):
|
|||
class MarkdownContext:
|
||||
"""Manages the console render state."""
|
||||
|
||||
def __init__(self, console: Console, options: ConsoleOptions, style: Style) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
console: Console,
|
||||
options: ConsoleOptions,
|
||||
style: Style,
|
||||
inline_code_lexer: str = None,
|
||||
inline_code_theme: str = "monokai",
|
||||
) -> None:
|
||||
self.console = console
|
||||
self.options = options
|
||||
self.style_stack: StyleStack = StyleStack(style)
|
||||
self.stack: Stack[MarkdownElement] = Stack()
|
||||
|
||||
self._syntax: Optional[Syntax] = None
|
||||
if inline_code_lexer is not None:
|
||||
self._syntax = Syntax("", inline_code_lexer, theme=inline_code_theme)
|
||||
|
||||
@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:
|
||||
def on_text(self, text: str, node_type: str) -> None:
|
||||
"""Called when the parser visits text."""
|
||||
self.stack.top.on_text(self, text)
|
||||
if node_type == "code" and self._syntax is not None:
|
||||
highlight_text = self._syntax.highlight(text)
|
||||
highlight_text.rstrip()
|
||||
self.stack.top.on_text(
|
||||
self, Text.assemble(highlight_text, style=self.style_stack.current)
|
||||
)
|
||||
else:
|
||||
self.stack.top.on_text(self, text)
|
||||
|
||||
def enter_style(self, style_name: Union[str, Style]) -> Style:
|
||||
"""Enter a style context."""
|
||||
|
@ -385,6 +397,10 @@ class Markdown(JupyterMixin):
|
|||
justify (JustifyMethod, optional): Justify value for paragraphs. Defaults to None.
|
||||
style (Union[str, Style], optional): Optional style to apply to markdown.
|
||||
hyperlinks (bool, optional): Enable hyperlinks. Defaults to ``True``.
|
||||
inline_code_lexer: (str, optional): Lexer to use if inline code highlighting is
|
||||
enabled. Defaults to "python".
|
||||
inline_code_theme: (Optional[str], optional): Pygments theme for inline code
|
||||
highlighting, or None for no highlighting. Defaults to None.
|
||||
"""
|
||||
|
||||
elements: ClassVar[Dict[str, Type[MarkdownElement]]] = {
|
||||
|
@ -406,6 +422,8 @@ class Markdown(JupyterMixin):
|
|||
justify: JustifyMethod = None,
|
||||
style: Union[str, Style] = "none",
|
||||
hyperlinks: bool = True,
|
||||
inline_code_lexer: str = None,
|
||||
inline_code_theme: str = None,
|
||||
) -> None:
|
||||
self.markup = markup
|
||||
parser = Parser()
|
||||
|
@ -414,26 +432,34 @@ class Markdown(JupyterMixin):
|
|||
self.justify = justify
|
||||
self.style = style
|
||||
self.hyperlinks = hyperlinks
|
||||
self.inline_code_lexer = inline_code_lexer
|
||||
self.inline_code_theme = inline_code_theme or code_theme
|
||||
|
||||
def __rich_console__(
|
||||
self, console: Console, options: ConsoleOptions
|
||||
) -> RenderResult:
|
||||
"""Render markdown to the console."""
|
||||
style = console.get_style(self.style, default="none")
|
||||
context = MarkdownContext(console, options, style)
|
||||
context = MarkdownContext(
|
||||
console,
|
||||
options,
|
||||
style,
|
||||
inline_code_lexer=self.inline_code_lexer,
|
||||
inline_code_theme=self.inline_code_theme,
|
||||
)
|
||||
nodes = self.parsed.walker()
|
||||
inlines = self.inlines
|
||||
new_line = False
|
||||
for current, entering in nodes:
|
||||
node_type = current.t
|
||||
if node_type in ("html_inline", "html_block", "text"):
|
||||
context.on_text(current.literal.replace("\n", " "))
|
||||
context.on_text(current.literal.replace("\n", " "), node_type)
|
||||
elif node_type == "linebreak":
|
||||
if entering:
|
||||
context.on_text("\n")
|
||||
context.on_text("\n", node_type)
|
||||
elif node_type == "softbreak":
|
||||
if entering:
|
||||
context.on_text(" ")
|
||||
context.on_text(" ", node_type)
|
||||
elif node_type == "link":
|
||||
if entering:
|
||||
link_style = console.get_style("markdown.link", default="none")
|
||||
|
@ -443,14 +469,14 @@ class Markdown(JupyterMixin):
|
|||
else:
|
||||
context.leave_style()
|
||||
if not self.hyperlinks:
|
||||
context.on_text(" (")
|
||||
context.on_text(" (", node_type)
|
||||
style = Style(underline=True) + console.get_style(
|
||||
"markdown.link_url", default="none"
|
||||
)
|
||||
context.enter_style(style)
|
||||
context.on_text(current.destination)
|
||||
context.on_text(current.destination, node_type)
|
||||
context.leave_style()
|
||||
context.on_text(")")
|
||||
context.on_text(")", node_type)
|
||||
elif node_type in inlines:
|
||||
if current.is_container():
|
||||
if entering:
|
||||
|
@ -460,7 +486,7 @@ class Markdown(JupyterMixin):
|
|||
else:
|
||||
context.enter_style(f"markdown.{node_type}")
|
||||
if current.literal:
|
||||
context.on_text(current.literal)
|
||||
context.on_text(current.literal, node_type)
|
||||
context.leave_style()
|
||||
else:
|
||||
element_class = self.elements.get(node_type) or UnknownElement
|
||||
|
@ -521,6 +547,13 @@ if __name__ == "__main__": # pragma: no cover
|
|||
default="monokai",
|
||||
help="pygments code theme",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-i",
|
||||
"--inline-code-lexer",
|
||||
dest="inline_code_lexer",
|
||||
default=None,
|
||||
help="inline_code_lexer",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-y",
|
||||
"--hyperlinks",
|
||||
|
@ -560,6 +593,7 @@ if __name__ == "__main__": # pragma: no cover
|
|||
justify="full" if args.justify else "left",
|
||||
code_theme=args.code_theme,
|
||||
hyperlinks=args.hyperlinks,
|
||||
inline_code_lexer=args.inline_code_lexer,
|
||||
)
|
||||
if args.page:
|
||||
import pydoc
|
||||
|
|
|
@ -11,7 +11,7 @@ from pygments.util import ClassNotFound
|
|||
|
||||
from ._loop import loop_first
|
||||
from .color import Color, blend_rgb, parse_rgb_hex
|
||||
from .console import Console, ConsoleOptions, RenderResult, Segment
|
||||
from .console import Console, ConsoleOptions, JustifyMethod, RenderResult, Segment
|
||||
from .jupyter import JupyterMixin
|
||||
from .measure import Measurement
|
||||
from .style import Style
|
||||
|
@ -173,18 +173,27 @@ class Syntax(JupyterMixin):
|
|||
style = style + Style(bgcolor=self._pygments_style_class.background_color)
|
||||
return style
|
||||
|
||||
def _highlight(self, lexer_name: str) -> Text:
|
||||
def highlight(self, code: str) -> Text:
|
||||
"""Highlight code and return a Text instance.
|
||||
|
||||
Args:
|
||||
code (str). Code to highlight.
|
||||
|
||||
Returns:
|
||||
Text: A text instance containing syntax highlight.
|
||||
"""
|
||||
|
||||
default_style = self._get_default_style()
|
||||
try:
|
||||
lexer = get_lexer_by_name(lexer_name)
|
||||
lexer = get_lexer_by_name(self.lexer_name)
|
||||
except ClassNotFound:
|
||||
return Text(
|
||||
self.code, justify="left", style=default_style, tab_size=self.tab_size
|
||||
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):
|
||||
for token_type, token in lexer.get_tokens(code):
|
||||
append(token, _get_theme_style(token_type))
|
||||
return text
|
||||
|
||||
|
@ -244,7 +253,7 @@ class Syntax(JupyterMixin):
|
|||
code = self.code
|
||||
if self.dedent:
|
||||
code = textwrap.dedent(code)
|
||||
text = self._highlight(self.lexer_name)
|
||||
text = self.highlight(self.code)
|
||||
if text.plain.endswith("\n"):
|
||||
text.plain = text.plain[:-1]
|
||||
if not self.line_numbers:
|
||||
|
|
15
rich/text.py
15
rich/text.py
|
@ -1,3 +1,4 @@
|
|||
from functools import partial
|
||||
import re
|
||||
from operator import itemgetter
|
||||
from typing import (
|
||||
|
@ -363,7 +364,7 @@ class Text(JupyterMixin):
|
|||
offset = len(self) + offset
|
||||
|
||||
get_style = console.get_style
|
||||
style = console.get_style(self.style).copy()
|
||||
style = get_style(self.style).copy()
|
||||
for start, end, span_style in self._spans:
|
||||
if offset >= start and offset < end:
|
||||
style += get_style(span_style)
|
||||
|
@ -506,14 +507,9 @@ class Text(JupyterMixin):
|
|||
"""
|
||||
|
||||
text = self.plain
|
||||
style_map: Dict[int, Style] = {}
|
||||
null_style = Style()
|
||||
|
||||
def get_style(style: Union[str, Style]) -> Style:
|
||||
return console.get_style(style, default=null_style)
|
||||
|
||||
enumerated_spans = list(enumerate(self._spans, 1))
|
||||
|
||||
get_style = partial(console.get_style, default=null_style)
|
||||
style_map = {index: get_style(span.style) for index, span in enumerated_spans}
|
||||
style_map[0] = get_style(self.style)
|
||||
|
||||
|
@ -530,14 +526,14 @@ class Text(JupyterMixin):
|
|||
stack_pop = stack.remove
|
||||
|
||||
_Segment = Segment
|
||||
|
||||
style_cache: Dict[Tuple[int, ...], Style] = {}
|
||||
style_cache_get = style_cache.get
|
||||
combine = Style.combine
|
||||
|
||||
def get_current_style() -> Style:
|
||||
"""Construct current style from stack."""
|
||||
style_ids = tuple(sorted(stack))
|
||||
cached_style = style_cache.get(style_ids)
|
||||
cached_style = style_cache_get(style_ids)
|
||||
if cached_style is not None:
|
||||
return cached_style
|
||||
current_style = combine(style_map[_style_id] for _style_id in style_ids)
|
||||
|
@ -879,7 +875,6 @@ class Text(JupyterMixin):
|
|||
(offset, offset + len(line))
|
||||
for offset, line in zip(divide_offsets, new_lines)
|
||||
]
|
||||
|
||||
for span in self._spans:
|
||||
line_index = (span.start // average_line_length) % len(line_ranges)
|
||||
|
||||
|
|
|
@ -97,6 +97,19 @@ def test_markdown_render():
|
|||
assert rendered_markdown == expected
|
||||
|
||||
|
||||
def test_inline_code():
|
||||
markdown = Markdown(
|
||||
"inline `import this` code",
|
||||
inline_code_lexer="python",
|
||||
inline_code_theme="emacs",
|
||||
)
|
||||
result = render(markdown)
|
||||
expected = "inline \x1b[1;38;2;170;34;255;48;2;248;248;248mimport\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m \x1b[0m\x1b[1;38;2;0;0;255;48;2;248;248;248mthis\x1b[0m code \n"
|
||||
print(result)
|
||||
print(repr(result))
|
||||
assert result == expected
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
markdown = Markdown(MARKDOWN)
|
||||
rendered = render(markdown)
|
||||
|
|
Loading…
Reference in New Issue