diff --git a/rich/markdown.py b/rich/markdown.py index 1e622776..991ad60b 100644 --- a/rich/markdown.py +++ b/rich/markdown.py @@ -18,7 +18,7 @@ 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 +45,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 +99,9 @@ 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 +350,36 @@ 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_theme: str = None, + inline_code_lexer: str = "python", + ) -> 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_theme 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, highlight_text) + else: + self.stack.top.on_text(self, text) def enter_style(self, style_name: Union[str, Style]) -> Style: """Enter a style context.""" @@ -406,6 +423,8 @@ class Markdown(JupyterMixin): justify: JustifyMethod = None, style: Union[str, Style] = "none", hyperlinks: bool = True, + inline_code_theme: Optional[str] = "monokai", + inline_code_lexer: str = "python", ) -> None: self.markup = markup parser = Parser() @@ -414,26 +433,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 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_theme=self.inline_code_theme, + inline_code_lexer=self.inline_code_lexer, + ) 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 +470,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 +487,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 @@ -500,77 +527,84 @@ class Markdown(JupyterMixin): if __name__ == "__main__": # pragma: no cover - import argparse + test = """# Hello +This is a test of inline code: `import this` , `for a in range(10):`""" + from rich import print - parser = argparse.ArgumentParser( - description="Render Markdown to the console with Rich" - ) - parser.add_argument("path", metavar="PATH", help="path to markdown file") - parser.add_argument( - "-c", - "--force-color", - dest="force_color", - action="store_true", - default=None, - help="force color for non-terminals", - ) - parser.add_argument( - "-t", - "--code-theme", - dest="code_theme", - default="monokai", - help="pygments code theme", - ) - parser.add_argument( - "-y", - "--hyperlinks", - dest="hyperlinks", - action="store_true", - help="enable hyperlinks", - ) - parser.add_argument( - "-w", - "--width", - type=int, - dest="width", - default=None, - help="width of output (default will auto-detect)", - ) - parser.add_argument( - "-j", - "--justify", - dest="justify", - action="store_true", - help="enable full text justify", - ) - parser.add_argument( - "-p", - "--page", - dest="page", - action="store_true", - help="use pager to scroll output", - ) - args = parser.parse_args() + print(Markdown(test, inline_code_theme="emacs")) - from rich.console import Console + if 0: + import argparse - with open(args.path, "rt", encoding="utf-8") as markdown_file: - markdown = Markdown( - markdown_file.read(), - justify="full" if args.justify else "left", - code_theme=args.code_theme, - hyperlinks=args.hyperlinks, + parser = argparse.ArgumentParser( + description="Render Markdown to the console with Rich" ) - if args.page: - import pydoc - import io - - console = Console( - file=io.StringIO(), force_terminal=args.force_color, width=args.width + parser.add_argument("path", metavar="PATH", help="path to markdown file") + parser.add_argument( + "-c", + "--force-color", + dest="force_color", + action="store_true", + default=None, + help="force color for non-terminals", ) - console.print(markdown) - pydoc.pager(console.file.getvalue()) # type: ignore + parser.add_argument( + "-t", + "--code-theme", + dest="code_theme", + default="monokai", + help="pygments code theme", + ) + parser.add_argument( + "-y", + "--hyperlinks", + dest="hyperlinks", + action="store_true", + help="enable hyperlinks", + ) + parser.add_argument( + "-w", + "--width", + type=int, + dest="width", + default=None, + help="width of output (default will auto-detect)", + ) + parser.add_argument( + "-j", + "--justify", + dest="justify", + action="store_true", + help="enable full text justify", + ) + parser.add_argument( + "-p", + "--page", + dest="page", + action="store_true", + help="use pager to scroll output", + ) + args = parser.parse_args() - else: - console = Console(force_terminal=args.force_color, width=args.width) - console.print(markdown) + from rich.console import Console + + with open(args.path, "rt", encoding="utf-8") as markdown_file: + markdown = Markdown( + markdown_file.read(), + justify="full" if args.justify else "left", + code_theme=args.code_theme, + hyperlinks=args.hyperlinks, + ) + if args.page: + import pydoc + import io + + console = Console( + file=io.StringIO(), force_terminal=args.force_color, width=args.width + ) + console.print(markdown) + pydoc.pager(console.file.getvalue()) # type: ignore + + else: + console = Console(force_terminal=args.force_color, width=args.width) + console.print(markdown) diff --git a/rich/syntax.py b/rich/syntax.py index c5dc951a..81628de8 100644 --- a/rich/syntax.py +++ b/rich/syntax.py @@ -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,25 @@ 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 = None) -> Text: + """Highlight a string with the given lexter and return a Text instance. + + Returns: + Text: A text instance containing highlights. + """ + if code is None: + code = self.code 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 +251,7 @@ class Syntax(JupyterMixin): code = self.code if self.dedent: code = textwrap.dedent(code) - text = self._highlight(self.lexer_name) + text = self.highlight() if text.plain.endswith("\n"): text.plain = text.plain[:-1] if not self.line_numbers: