inline code in Markdown

This commit is contained in:
Will McGugan 2020-09-13 12:57:45 +01:00
parent 857d8e0b6c
commit c50751c774
2 changed files with 130 additions and 89 deletions

View File

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

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