Merge remote

This commit is contained in:
Hedy Li 2020-09-16 07:10:39 +00:00
commit a8a2e1d4b3
9 changed files with 100 additions and 40 deletions

View File

@ -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

View File

@ -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()))

View File

@ -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)

View File

@ -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"

View File

@ -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."""

View File

@ -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

View File

@ -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:

View File

@ -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)

View File

@ -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)