From 18b48c13312832b888bf1c338f6d56cfbf946365 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 28 Jan 2026 02:22:00 +0000 Subject: [PATCH] feat: Add text_align option to Style class - Add TextAlign literal type (left, center, right) - Add text_align field to Style class with default 'left' - Add get_text_anchor() helper method to map to SVG text-anchor values - Update SVGRenderer._render_text_block to apply text-anchor attribute - Calculate x position based on alignment (left edge, center, right edge) - Export TextAlign and CodeBlockOverflow types from __init__.py - Add tests for all alignment options - Update README with text alignment example and documentation Co-authored-by: davefowler --- README.md | 20 ++++++++++++++++++++ src/mdsvg/__init__.py | 5 +++++ src/mdsvg/renderer.py | 14 ++++++++++++-- src/mdsvg/style.py | 16 ++++++++++++++++ tests/test_renderer.py | 39 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 92 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 167f325..fca5c8a 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,25 @@ style = Style( svg = render("# Styled Heading", style=style) ``` +### Text Alignment + +Control horizontal text alignment using the `text_align` option: + +```python +from mdsvg import render, Style + +# Center-aligned text (great for titles in dashboards) +style = Style(text_align="center") +svg = render("# Centered Title", style=style) + +# Right-aligned text +style = Style(text_align="right") +svg = render("Right aligned content", style=style) + +# Default is left-aligned +style = Style(text_align="left") +``` + ### Built-in Themes ```python @@ -263,6 +282,7 @@ renderer = SVGRenderer(use_precise_measurement=False) | `paragraph_spacing` | 12.0 | Space between paragraphs (px) | | `list_indent` | 24.0 | List indentation (px) | | `char_width_ratio` | 0.48 | Average character width ratio | +| `text_align` | "left" | Horizontal text alignment ("left", "center", "right") | ## Playground diff --git a/src/mdsvg/__init__.py b/src/mdsvg/__init__.py index fef6ff3..694086d 100644 --- a/src/mdsvg/__init__.py +++ b/src/mdsvg/__init__.py @@ -45,8 +45,10 @@ GITHUB_THEME, LIGHT_THEME, MINIMAL_PRESET, + CodeBlockOverflow, Style, StylePresets, + TextAlign, merge_styles, ) from .types import ( @@ -92,6 +94,9 @@ "COMPACT_PRESET", "MINIMAL_PRESET", "merge_styles", + # Style option types + "TextAlign", + "CodeBlockOverflow", # Types "Size", "TextMetrics", diff --git a/src/mdsvg/renderer.py b/src/mdsvg/renderer.py index 7a4b5af..9689d56 100644 --- a/src/mdsvg/renderer.py +++ b/src/mdsvg/renderer.py @@ -892,6 +892,15 @@ def _render_text_block( current_y = ctx.y + font_size # Baseline + # Calculate x position based on text alignment + text_anchor = self.style.get_text_anchor() + if self.style.text_align == "center": + text_x = ctx.x + ctx.width / 2 + elif self.style.text_align == "right": + text_x = ctx.x + ctx.width + else: # left (default) + text_x = ctx.x + for line_runs in lines: if not line_runs: current_y += line_height @@ -945,8 +954,9 @@ def _render_text_block( # Build the complete text element text_content = "".join(tspan_parts) text_element = ( - f' ' + f' ' f"{text_content}" ) elements.append(text_element) diff --git a/src/mdsvg/style.py b/src/mdsvg/style.py index 403dd71..c9a7d38 100644 --- a/src/mdsvg/style.py +++ b/src/mdsvg/style.py @@ -8,6 +8,9 @@ # Code block overflow options CodeBlockOverflow = Literal["wrap", "show", "hide", "ellipsis"] +# Text alignment options +TextAlign = Literal["left", "center", "right"] + @dataclass(frozen=True) class Style: @@ -58,6 +61,7 @@ class Style: char_width_ratio: Average character width as ratio of font size. bold_char_width_ratio: Character width ratio for bold text. mono_char_width_ratio: Character width ratio for monospace text. + text_align: Horizontal text alignment ("left", "center", "right"). """ # Fonts @@ -119,6 +123,9 @@ class Style: mono_char_width_ratio: float = 0.6 # Mono char width = font_size × ratio (all chars identical) text_width_scale: float = 1.1 # Safety margin for browser rendering differences + # Text alignment + text_align: TextAlign = "left" # Horizontal alignment: "left", "center", "right" + def with_updates(self, **kwargs: Any) -> Style: """ Create a new Style with updated values. @@ -162,6 +169,15 @@ def get_heading_color(self) -> str: """Get the color for headings, falling back to text_color.""" return self.heading_color or self.text_color + def get_text_anchor(self) -> str: + """Get the SVG text-anchor value corresponding to text_align.""" + anchor_map = { + "left": "start", + "center": "middle", + "right": "end", + } + return anchor_map.get(self.text_align, "start") + # Pre-built themes LIGHT_THEME = Style() diff --git a/tests/test_renderer.py b/tests/test_renderer.py index 5bbbb4f..e9fc663 100644 --- a/tests/test_renderer.py +++ b/tests/test_renderer.py @@ -244,6 +244,45 @@ def test_very_long_text(self) -> None: assert svg.count(" 1 +class TestTextAlignment: + """Test text alignment functionality.""" + + def test_default_alignment_is_left(self) -> None: + """Test default text alignment is left (start).""" + svg = render("Hello") + assert 'text-anchor="start"' in svg + + def test_center_alignment(self) -> None: + """Test center text alignment.""" + style = Style(text_align="center") + svg = render("Hello", style=style) + assert 'text-anchor="middle"' in svg + + def test_right_alignment(self) -> None: + """Test right text alignment.""" + style = Style(text_align="right") + svg = render("Hello", style=style) + assert 'text-anchor="end"' in svg + + def test_left_alignment_explicit(self) -> None: + """Test explicit left text alignment.""" + style = Style(text_align="left") + svg = render("Hello", style=style) + assert 'text-anchor="start"' in svg + + def test_heading_respects_alignment(self) -> None: + """Test that headings respect text alignment.""" + style = Style(text_align="center") + svg = render("# Centered Title", style=style) + assert 'text-anchor="middle"' in svg + + def test_style_get_text_anchor_method(self) -> None: + """Test Style.get_text_anchor() helper method.""" + assert Style(text_align="left").get_text_anchor() == "start" + assert Style(text_align="center").get_text_anchor() == "middle" + assert Style(text_align="right").get_text_anchor() == "end" + + class TestComplexDocuments: """Test complex document rendering."""