diff --git a/src/toad/line_blocks.py b/src/toad/line_blocks.py index 3c9f887cc..11a2e8927 100644 --- a/src/toad/line_blocks.py +++ b/src/toad/line_blocks.py @@ -114,6 +114,12 @@ class OutgoingRole(LineRole): colour = SuccessColour +class GreenTextRole(LineRole): + """A quoted line, drawn as greentext.""" + + colour = SuccessColour + + class NoticeRole(LineRole): colour = WarningColour diff --git a/src/toad/markdown_links.py b/src/toad/markdown_links.py index 91b237c63..43618e003 100644 --- a/src/toad/markdown_links.py +++ b/src/toad/markdown_links.py @@ -4,7 +4,7 @@ import re from abc import abstractmethod -from dataclasses import dataclass +from dataclasses import dataclass, field from functools import partial from pathlib import Path from threading import local @@ -77,10 +77,28 @@ def matches(self, text: str): return sorted(found) +class JoinedLines: + """Paragraph source lines join with a space, as Markdown draws them.""" + + @staticmethod + def softbreak(token: Token) -> Token: + return token + + +class QuotedLines(JoinedLines): + """Every quoted source line stays its own line, as greentext.""" + + @staticmethod + def softbreak(token: Token) -> Token: + return Token("hardbreak", "br", 0) + + @dataclass(slots=True) class _ProjectTokens: links: LinkContext linked: int = 0 + # The innermost open block decides how a source newline inside it is drawn. + lines: list[type[JoinedLines]] = field(default_factory=lambda: [JoinedLines]) class ProjectTokenRule(DeclaredFamily, affix="TokenRule"): @@ -93,10 +111,30 @@ class InlineTokenRule(ProjectTokenRule): @classmethod def resolve(cls, renderer: ProjectTokenRenderer, token: Token, context: _ProjectTokens): if token.children: - token.children = renderer.resolve(token.children, _ProjectTokens(context.links)) + token.children = renderer.resolve(token.children, _ProjectTokens(context.links, lines=context.lines)) + return [token] + + +class BlockquoteOpenTokenRule(ProjectTokenRule): + @classmethod + def resolve(cls, renderer: ProjectTokenRenderer, token: Token, context: _ProjectTokens): + context.lines.append(QuotedLines) + return [token] + + +class BlockquoteCloseTokenRule(ProjectTokenRule): + @classmethod + def resolve(cls, renderer: ProjectTokenRenderer, token: Token, context: _ProjectTokens): + context.lines.pop() return [token] +class SoftbreakTokenRule(ProjectTokenRule): + @classmethod + def resolve(cls, renderer: ProjectTokenRenderer, token: Token, context: _ProjectTokens): + return [context.lines[-1].softbreak(token)] + + class LinkOpenTokenRule(ProjectTokenRule): @classmethod def resolve(cls, renderer: ProjectTokenRenderer, token: Token, context: _ProjectTokens): diff --git a/src/toad/markdown_preparation.py b/src/toad/markdown_preparation.py index f1650f80b..2987e483d 100644 --- a/src/toad/markdown_preparation.py +++ b/src/toad/markdown_preparation.py @@ -6,9 +6,11 @@ from markdown_it.token import Token from rich.console import Console -from rich.markdown import CodeBlock, Markdown +from rich.markdown import BlockQuote, CodeBlock, Markdown from rich.padding import Padding +from rich.segment import Segment from rich.syntax import Syntax +from toad.line_blocks import GreenTextRole from toad.markdown_links import MessageMarkdownParser, parse_markdown_syntax @@ -24,9 +26,27 @@ def __rich_console__(self, console, options): yield Padding(self.text, 1, style=self.text.style) +class GreenText(BlockQuote): + """A quoted line keeps its `>` and reads in the greentext role's colour.""" + + def on_enter(self, context): + self.style = context.enter_style(context.console.line_theme.style(GreenTextRole)) + + def __rich_console__(self, console, options): + marker, new_line = Segment("> ", self.style), Segment("\n") + for line in console.render_lines(self.elements, options.update(width=options.max_width - 2), style=self.style): + yield marker + yield from line + yield new_line + + class MarkdownStyleConsole(Console): """Rich builds its styled element tree; width-dependent rendering runs later.""" + def __init__(self, line_theme, **options): + super().__init__(color_system=line_theme.color_system, **options) + self.line_theme = line_theme + def render(self, renderable, options=None): self.parts.append(renderable) return iter(()) @@ -34,7 +54,8 @@ def render(self, renderable, options=None): def prepare(self, text, context): self.parts = [] markdown = Markdown("") - markdown.elements = {**markdown.elements, "fence": StyledCodeBlock, "code_block": StyledCodeBlock} + markdown.elements = {**markdown.elements, "fence": StyledCodeBlock, "code_block": StyledCodeBlock, + "blockquote_open": GreenText} markdown.parsed = MessageMarkdownParser(context).parse(text) for segment in markdown.__rich_console__(self, self.options): self.parts.append(segment) diff --git a/src/toad/widgets/transcript_lines.py b/src/toad/widgets/transcript_lines.py index c5d26db72..d454e6edf 100644 --- a/src/toad/widgets/transcript_lines.py +++ b/src/toad/widgets/transcript_lines.py @@ -729,9 +729,7 @@ def body(self, block: Body) -> LineBlock: return block.prepare(self.markdown) def markdown(self, block: Body) -> StyledBody: - console = MarkdownStyleConsole( - force_terminal=True, file=io.StringIO(), color_system=self.preparation.theme.color_system - ) + console = MarkdownStyleConsole(self.preparation.theme, force_terminal=True, file=io.StringIO()) return StyledBody(block, console.prepare(block.text, self.context)) @handles(WireBody)