From f6c1c62a104244032c4f9819e5664dbfb2bf9b4a Mon Sep 17 00:00:00 2001 From: Nils Nordman Date: Mon, 11 Aug 2025 20:07:13 +0200 Subject: [PATCH 1/2] Add option for controlling justification for Markdown headers --- docs/source/markdown.rst | 34 ++++++++++++++++++++++++++++- examples/markdown_justify.py | 42 ++++++++++++++++++++++++++++++++++++ rich/markdown.py | 10 ++++++--- 3 files changed, 82 insertions(+), 4 deletions(-) create mode 100644 examples/markdown_justify.py diff --git a/docs/source/markdown.rst b/docs/source/markdown.rst index 927c8597c..59064a26f 100644 --- a/docs/source/markdown.rst +++ b/docs/source/markdown.rst @@ -20,10 +20,42 @@ Rich can render Markdown to the console. To render markdown, construct a :class: Note that code blocks are rendered with full syntax highlighting! +Justification +------------- + +You can control the alignment of both paragraphs and headers in Markdown using the ``justify`` and ``justify_headers`` parameters respectively. + +Here's an example showing different justification options:: + + from rich.console import Console + from rich.markdown import Markdown + + console = Console(width=60) + + markdown_text = """ + # Left Justified Header + + This paragraph will be center justified, while the header above is left justified. + + ## Right Justified Subheader + + This paragraph will be right justified, while the subheader above is right justified. + """ + + # Left-justify headers, center-justify paragraphs + md = Markdown(markdown_text, justify="center", justify_headers="left") + console.print(md) + + # Right-justify headers, right-justify paragraphs + md = Markdown(markdown_text, justify="right", justify_headers="right") + console.print(md) + +The ``justify`` parameter controls paragraph alignment and accepts ``"left"``, ``"center"``, or ``"right"``. The ``justify_headers`` parameter controls header alignment with the same options. If not specified, paragraphs default to left alignment and headers default to center alignment. + You can also use the Markdown class from the command line. The following example displays a readme in the terminal:: python -m rich.markdown README.md Run the following to see the full list of arguments for the markdown command:: - python -m rich.markdown -h \ No newline at end of file + python -m rich.markdown -h diff --git a/examples/markdown_justify.py b/examples/markdown_justify.py new file mode 100644 index 000000000..2ed0f016a --- /dev/null +++ b/examples/markdown_justify.py @@ -0,0 +1,42 @@ +""" +This example demonstrates the justify and justify_headers arguments in Markdown. +""" + +from rich.console import Console +from rich.markdown import Markdown + +console = Console(width=60) + +markdown_content = """ +# Main Title + +This is a paragraph under the main title. The justification of this paragraph and the header can be controlled independently. + +## Section Title + +This is another paragraph that demonstrates how different justification settings can create different visual layouts. + +### Subsection Title + +Final paragraph showing the combined effect of paragraph and header justification settings. +""" + +print("=== Default justification (headers: center, paragraphs: left) ===") +md_default = Markdown(markdown_content) +console.print(md_default) +print() + +print("=== Left-justified headers, center-justified paragraphs ===") +md_left_center = Markdown(markdown_content, justify="center", justify_headers="left") +console.print(md_left_center) +print() + +print("=== Right-justified headers, right-justified paragraphs ===") +md_right_right = Markdown(markdown_content, justify="right", justify_headers="right") +console.print(md_right_right) +print() + +print("=== Center-justified headers, left-justified paragraphs ===") +md_center_left = Markdown(markdown_content, justify="left", justify_headers="center") +console.print(md_center_left) +print() diff --git a/rich/markdown.py b/rich/markdown.py index 12496487b..3ac688cb7 100644 --- a/rich/markdown.py +++ b/rich/markdown.py @@ -129,22 +129,23 @@ class Heading(TextElement): @classmethod def create(cls, markdown: Markdown, token: Token) -> Heading: - return cls(token.tag) + return cls(token.tag, justify=markdown.justify_headers or "center") def on_enter(self, context: MarkdownContext) -> None: self.text = Text() context.enter_style(self.style_name) - def __init__(self, tag: str) -> None: + def __init__(self, tag: str, justify: JustifyMethod) -> None: self.tag = tag self.style_name = f"markdown.{tag}" + self.justify = justify super().__init__() def __rich_console__( self, console: Console, options: ConsoleOptions ) -> RenderResult: text = self.text - text.justify = "center" + text.justify = self.justify if self.tag == "h1": # Draw a border around h1s yield Panel( @@ -502,6 +503,7 @@ class Markdown(JupyterMixin): markup (str): A string containing markdown. code_theme (str, optional): Pygments theme for code blocks. Defaults to "monokai". See https://pygments.org/styles/ for code themes. justify (JustifyMethod, optional): Justify value for paragraphs. Defaults to None. + justify_headers (JustifyMethod, optional): Justify value for headers. 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 @@ -536,6 +538,7 @@ def __init__( markup: str, code_theme: str = "monokai", justify: JustifyMethod | None = None, + justify_headers: JustifyMethod | None = None, style: str | Style = "none", hyperlinks: bool = True, inline_code_lexer: str | None = None, @@ -546,6 +549,7 @@ def __init__( self.parsed = parser.parse(markup) self.code_theme = code_theme self.justify: JustifyMethod | None = justify + self.justify_headers: JustifyMethod | None = justify_headers self.style = style self.hyperlinks = hyperlinks self.inline_code_lexer = inline_code_lexer From b0b52ce4a6ec60b107d325414921f5a811215c5e Mon Sep 17 00:00:00 2001 From: Nils Nordman Date: Mon, 11 Aug 2025 20:19:00 +0200 Subject: [PATCH 2/2] Add some (AI) tests --- tests/test_markdown.py | 223 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 223 insertions(+) diff --git a/tests/test_markdown.py b/tests/test_markdown.py index 2ffbdd93d..419254c9e 100644 --- a/tests/test_markdown.py +++ b/tests/test_markdown.py @@ -199,6 +199,229 @@ def test_table_with_empty_cells() -> None: assert result == expected +def test_header_justification_default(): + """Test that headers are center-justified by default.""" + markdown = Markdown( + """\ +# Main Title + +## Section Title + +### Subsection Title + +This is a paragraph. +""" + ) + result = render(markdown) + # Check that headers are centered (padding on both sides) + assert ( + "┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓" + in result + ) + assert ( + "┃ \x1b[1mMain Title\x1b[0m ┃" + in result + ) + assert ( + " \x1b[1;4mSection Title\x1b[0m " + in result + ) + assert ( + " \x1b[1mSubsection Title\x1b[0m " + in result + ) + + +def test_header_justification_left(): + """Test left justification for headers.""" + markdown = Markdown( + """\ +# Main Title + +## Section Title + +### Subsection Title + +This is a paragraph. +""", + justify_headers="left", + ) + result = render(markdown) + # Check that h1 is left-justified (no leading spaces in panel) + assert ( + "┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓" + in result + ) + assert ( + "┃ \x1b[1mMain Title\x1b[0m ┃" + in result + ) + # Check that h2 and h3 are left-justified + assert ( + "\x1b[1;4mSection Title\x1b[0m " + in result + ) + assert ( + "\x1b[1mSubsection Title\x1b[0m " + in result + ) + + +def test_header_justification_right(): + """Test right justification for headers.""" + markdown = Markdown( + """\ +# Main Title + +## Section Title + +### Subsection Title + +This is a paragraph. +""", + justify_headers="right", + ) + result = render(markdown) + # Check that h1 is right-justified (trailing spaces in panel) + assert ( + "┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓" + in result + ) + assert ( + "┃ \x1b[1mMain Title\x1b[0m ┃" + in result + ) + # Check that h2 and h3 are right-justified + assert ( + " \x1b[1;4mSection Title\x1b[0m" + in result + ) + assert ( + " \x1b[1mSubsection Title\x1b[0m" + in result + ) + + +def test_header_justification_all_levels(): + """Test header justification for all header levels.""" + markdown = Markdown( + """\ +# H1 Header + +## H2 Header + +### H3 Header + +#### H4 Header + +##### H5 Header + +###### H6 Header + +Paragraph text. +""", + justify_headers="left", + ) + result = render(markdown) + # All headers should be left-justified + assert ( + "┃ \x1b[1mH1 Header\x1b[0m ┃" + in result + ) + assert ( + "\x1b[1;4mH2 Header\x1b[0m " + in result + ) + assert ( + "\x1b[1mH3 Header\x1b[0m " + in result + ) + assert ( + "\x1b[1;2mH4 Header\x1b[0m " + in result + ) + assert ( + "\x1b[4mH5 Header\x1b[0m " + in result + ) + assert ( + "\x1b[3mH6 Header\x1b[0m " + in result + ) + + +def test_header_justification_independent_from_paragraph(): + """Test that header justification works independently from paragraph justification.""" + markdown = Markdown( + """\ +# Centered Header + +This paragraph should be left-justified while the header above is centered. + +## Another Centered Header + +This paragraph should be left-justified while the header above is centered. +""", + justify_headers="center", + justify="left", + ) + result = render(markdown) + # Headers should be centered + assert ( + "┃ \x1b[1mCentered Header\x1b[0m ┃" + in result + ) + assert ( + " \x1b[1;4mAnother Centered Header\x1b[0m " + in result + ) + # Paragraphs should be left-justified + assert ( + "This paragraph should be left-justified while the header above is centered. " + in result + ) + assert ( + "This paragraph should be left-justified while the header above is centered. " + in result + ) + + +def test_header_justification_mixed(): + """Test mixed justification: right headers with center paragraphs.""" + markdown = Markdown( + """\ +# Right Header + +This paragraph should be center-justified while the header above is right-justified. + +## Another Right Header + +This paragraph should also be center-justified. +""", + justify_headers="right", + justify="center", + ) + result = render(markdown) + # Headers should be right-justified + assert ( + "┃ \x1b[1mRight Header\x1b[0m ┃" + in result + ) + assert ( + " \x1b[1;4mAnother Right Header\x1b[0m" + in result + ) + # Paragraphs should be center-justified (check for padding on both sides) + assert ( + " This paragraph should be center-justified while the header above is right-justified. " + in result + ) + assert ( + " This paragraph should also be center-justified. " + in result + ) + + if __name__ == "__main__": markdown = Markdown(MARKDOWN) rendered = render(markdown)