From 7c53168fb6c3fc08239f777abf8300ce11191ce5 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sun, 17 Nov 2019 17:15:23 +0000 Subject: [PATCH] better markdown renderer --- rich/default_styles.py | 3 + rich/markdown.py | 312 ++++++++++++++++++++++++++++++----------- rich/style.py | 23 ++- 3 files changed, 258 insertions(+), 80 deletions(-) diff --git a/rich/default_styles.py b/rich/default_styles.py index f404d0a6..a42b701b 100644 --- a/rich/default_styles.py +++ b/rich/default_styles.py @@ -43,6 +43,9 @@ MARKDOWN_STYLES = { "markdown.code_block": Style( "markdown.code_block", dim=True, color="cyan", back="black" ), + "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), diff --git a/rich/markdown.py b/rich/markdown.py index 271a32d0..f80b409e 100644 --- a/rich/markdown.py +++ b/rich/markdown.py @@ -1,106 +1,250 @@ +from __future__ import annotations + from dataclasses import dataclass -from typing import Dict, Iterable, List, Optional +from typing import Dict, Iterable, List, Optional, Union from commonmark.blocks import Parser -from .console import Console, ConsoleOptions, RenderResult, StyledText -from .style import Style +from .console import ( + Console, + ConsoleOptions, + ConsoleRenderable, + RenderResult, + StyledText, +) +from .style import Style, StyleStack from .text import Text from ._stack import Stack -class Heading(Text): - def __init__(self, level: int) -> None: - super().__init__() - self.level = level +class MarkdownElement: + def on_enter(self, context: MarkdownContext, node): + pass + + def on_text(self, context: MarkdownContext, text: str,) -> RenderResult: + pass + + def on_leave(self, context: MarkdownContext): + return + yield + + def on_child_close(self, context: MarkdownContext, child: MarkdownElement): + pass + + +class UnknownElement(MarkdownElement): + pass + + +class TextElement(MarkdownElement): + + style_name = "none" + + def __init__(self) -> None: + self.text = Text() + + def on_enter(self, context: MarkdownContext, node) -> None: + context.enter_style(f"markdown.h{node.level}") + + def on_text(self, context: MarkdownContext, text: str): + self.text.append(text, context.current_style) + + def on_leave(self, context: MarkdownContext) -> Iterable[Text]: + context.leave_style() + yield self.text + + def on_child_close(self, context: MarkdownContext, child: MarkdownElement): + pass + + +class Paragraph(TextElement): + pass + + +class Heading(TextElement): + def on_leave(self, context: MarkdownContext) -> Iterable[Text]: + context.leave_style() + lines = self.text.wrap(context.options.max_width, justify="center") + yield lines + + +class MarkdownContext: + def __init__(self, console: Console, options: ConsoleOptions) -> None: + self.console = console + self.options = options + self.style_stack: StyleStack = StyleStack(console.current_style) + self.stack: Stack[MarkdownElement] = Stack() + + @property + def current_style(self) -> Style: + return self.style_stack.current + + def on_text(self, text: str) -> None: + 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") + self.style_stack.push(style) + + def leave_style(self) -> Style: + style = self.style_stack.pop() + return style class Markdown: - """Render markdown to the console.""" - def __init__(self, markup): + elements = {"paragraph": Paragraph, "heading": Heading} + + def __init__(self, markup: str) -> None: self.markup = markup + parser = Parser() + self.parsed = parser.parse(markup) def __console__(self, console: Console, options: ConsoleOptions) -> RenderResult: - parser = Parser() + context = MarkdownContext(console, options) - nodes = parser.parse(self.markup).walker() + inline_styles = {"emph"} - rendered: List[StyledText] = [] - style_stack: Stack[Style] = Stack() - stack: Stack[Text] = Stack() - style_stack.push(Style()) - - null_style = Style() - - def push_style(name: str) -> Style: - - style = console.get_style(name) or null_style - style = style_stack.top.apply(style) - style_stack.push(style) - return style - - def pop_style() -> Style: - return style_stack.pop() - - paragraph_count = 0 + nodes = self.parsed.walker() for current, entering in nodes: - + print(current, entering) node_type = current.t if node_type == "text": - style = push_style("markdown.text") - stack.top.append(current.literal, style) - pop_style() - elif node_type == "paragraph": + context.on_text(current.literal) + elif node_type in inline_styles: if entering: - if paragraph_count: - yield StyledText("\n") - paragraph_count += 1 - push_style("markdown.paragraph") - stack.push(Text()) + context.enter_style(f"markdown.{node_type}") else: - pop_style() - text = stack.pop() - yield text.wrap(options.max_width) - # yield StyledText("\n") - - # yield StyledText("\n") - elif node_type == "heading": - if entering: - push_style(f"markdown.h{current.level}") - stack.push(Heading(current.level)) - else: - pop_style() - text = stack.pop() - yield text.wrap(options.max_width, justify="center") - yield StyledText("\n") - elif node_type == "code_block": - style = push_style("markdown.code_block") - text = Text(current.literal.rstrip(), style=style) - wrapped_text = text.wrap(options.max_width, justify="left") - yield StyledText("\n") - yield wrapped_text - pop_style() - - elif node_type == "code": - style = push_style("markdown.code") - stack.top.append(current.literal, style) - pop_style() - elif node_type == "softbreak": - stack.top.append("\n") - elif node_type == "thematic_break": - style = push_style("markdown.hr") - yield StyledText(f"\n{'—' * options.max_width}\n", style) - paragraph_count = 0 - pop_style() + context.leave_style() else: - if entering: - push_style(f"markdown.{node_type}") - else: - pop_style() + element_class = self.elements.get(node_type) or UnknownElement - yield from rendered + if entering: + element = element_class() + context.stack.push(element) + element.on_enter(context, current) + else: + element = context.stack.pop() + yield from element.on_leave(context) + if context.stack: + context.stack.top.on_child_close(context, element) + + +# class Markdown: +# """Render markdown to the console.""" + +# def __init__(self, markup): +# self.markup = markup + +# def __console__(self, console: Console, options: ConsoleOptions) -> RenderResult: + +# parser = Parser() + +# nodes = parser.parse(self.markup).walker() + +# rendered: List[StyledText] = [] +# style_stack: Stack[Style] = Stack() +# style_stack.push(Style()) +# stack = Stack() + +# text = Text() + +# null_style = Style() + +# def push_style(name: str) -> Style: +# """Enter in to a new style context.""" +# style = console.get_style(name) or null_style +# style = style_stack.top.apply(style) +# style_stack.push(style) +# return style + +# def pop_style() -> Style: +# """Leave a style context.""" +# return style_stack.pop() + +# deferred_space = False + +# def new_content( +# *objects: Union[ConsoleRenderable, StyledText, str], line=True +# ) -> RenderResult: +# nonlocal deferred_space +# if deferred_space and line: +# yield StyledText("\n") +# for render_object in objects: +# if isinstance(render_object, str): +# yield StyledText(render_object) +# elif isinstance(render_object, StyledText): +# yield render_object +# else: +# yield render_object +# deferred_space = True + +# element_stack: Stack[MarkdownElement] = Stack() +# for current, entering in nodes: +# node_type = current.t +# if node_type == "text": +# element_stack.top.on_text(current.literal) +# elif node_type == "emph": +# if entering: +# push_s + +# # for current, entering in nodes: +# # print(current, entering) +# # node_type = current.t +# # if node_type == "text": +# # style = push_style("markdown.text") +# # text.append(current.literal, style) +# # pop_style() +# # elif node_type == "paragraph": +# # if entering: +# # push_style("markdown.paragraph") +# # else: +# # pop_style() +# # yield from new_content(text.wrap(options.max_width)) +# # text = Text() +# # elif node_type == "heading": +# # if entering: +# # push_style(f"markdown.h{current.level}") +# # else: +# # pop_style() +# # yield from new_content( +# # text.wrap(options.max_width, justify="center") +# # ) +# # text = Text() +# # elif node_type == "code_block": +# # style = push_style("markdown.code_block") +# # code_text = Text(current.literal.rstrip(), style=style) +# # wrapped_text = code_text.wrap(options.max_width, justify="left") +# # yield from new_content(wrapped_text) +# # pop_style() +# # elif node_type == "code": +# # style = push_style("markdown.code") +# # text.append(current.literal, style) +# # pop_style() +# # elif node_type == "softbreak": +# # text.append("\n") +# # elif node_type == "thematic_break": +# # style = push_style("markdown.hr") +# # yield from new_content(StyledText(f"{'—' * options.max_width}", style)) +# # pop_style() +# # elif node_type == "block_quote": +# # if entering: +# # push_style("markdown.block_quote") +# # else: +# # style = pop_style() +# # elif node_type == "list": +# # if entering: +# # push_style("markdown.list") +# # else: +# # pop_style() +# # elif node_type == "item": +# # if entering: +# # push_style("markdown.item") +# # else: +# # pop_style() + +# yield from rendered markup = """ @@ -123,9 +267,19 @@ The main area where I think *Django's models* are `missing` out is the lack of t 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. --- - > This is a *block* quote > With another line + + * foo + * bar + * baz +""" + +markup = """\ +# Heading + +Hello, *World*! + """ if __name__ == "__main__": diff --git a/rich/style.py b/rich/style.py index e81f0067..c8be7859 100644 --- a/rich/style.py +++ b/rich/style.py @@ -1,7 +1,7 @@ from __future__ import annotations import sys -from typing import Dict, Iterable, List, Mapping, Optional, Type +from typing import Any, Dict, Iterable, List, Mapping, Optional, Type from . import errors from .color import Color @@ -290,6 +290,27 @@ class Style: return new_style +class StyleStack: + """A stack of styles that maintains a current style.""" + + def __init__(self, default_style: Style) -> None: + self._stack: List[Style] = [] + self._stack.append(default_style) + self.current = default_style + + def __repr__(self) -> str: + return f"" + + def push(self, style: Style) -> None: + self.current = self.current.apply(style) + self._stack.append(self.current) + + def pop(self) -> Style: + self._stack.pop() + self.current = self._stack[-1] + return self.current + + RESET_STYLE = Style.reset() if __name__ == "__main__":