Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 33 additions & 1 deletion docs/source/markdown.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
python -m rich.markdown -h
42 changes: 42 additions & 0 deletions examples/markdown_justify.py
Original file line number Diff line number Diff line change
@@ -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()
10 changes: 7 additions & 3 deletions rich/markdown.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand Down
223 changes: 223 additions & 0 deletions tests/test_markdown.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down