Add SVG CSS: dominant-baseline and text-rendering for proper alignment
- Added dominant-baseline: text-before-edge for proper vertical text positioning - Added text-rendering: optimizeLegibility for crisper text - Simplified y-position calculation (top-aligned with baseline) - Added tests for box drawing character detection helpers - Added test for CSS properties - Removed unreachable dead code paths (empty span checks) - svg_exporter.py now has 100% test coverage Version bump to 0.3.7
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "textual-webterm"
|
||||
version = "0.3.6"
|
||||
version = "0.3.7"
|
||||
description = "Serve terminal sessions over the web"
|
||||
authors = ["Will McGugan <will@textualize.io>"]
|
||||
license = "MIT"
|
||||
|
||||
@@ -146,6 +146,8 @@ def render_terminal_svg(
|
||||
f"font-size: {font_size}px; "
|
||||
f"fill: {foreground}; "
|
||||
f"white-space: pre; "
|
||||
f"dominant-baseline: text-before-edge; "
|
||||
f"text-rendering: optimizeLegibility; "
|
||||
f"}}"
|
||||
f".bold {{ font-weight: bold; }}"
|
||||
f".italic {{ font-style: italic; }}"
|
||||
@@ -164,7 +166,8 @@ def render_terminal_svg(
|
||||
|
||||
# Render each row
|
||||
for row_idx, row_data in enumerate(screen_buffer):
|
||||
y = 10 + (row_idx + 1) * actual_line_height - (actual_line_height - font_size) / 2
|
||||
# With dominant-baseline: text-before-edge, text top aligns to y
|
||||
y = 10 + row_idx * actual_line_height
|
||||
|
||||
# Build spans for this row, grouping consecutive chars with same style
|
||||
spans = _build_row_spans(row_data, foreground, background)
|
||||
@@ -178,24 +181,16 @@ def render_terminal_svg(
|
||||
|
||||
x = 10.0 # Starting x position with padding
|
||||
for span in spans:
|
||||
text = span["text"]
|
||||
columns = span["columns"]
|
||||
|
||||
# Background needs a separate rect (collected before text)
|
||||
if span["has_bg"] and span["bg"] != background:
|
||||
bg_width = columns * char_width
|
||||
bg_y = y - font_size + 2
|
||||
row_bg_rects.append(
|
||||
f'<rect x="{x:.1f}" y="{bg_y:.1f}" '
|
||||
f'<rect x="{x:.1f}" y="{y:.1f}" '
|
||||
f'width="{bg_width:.1f}" height="{actual_line_height:.1f}" '
|
||||
f'fill="{span["bg"]}"/>'
|
||||
)
|
||||
|
||||
if not text:
|
||||
# Skip truly empty spans (wide char placeholders already counted)
|
||||
x += columns * char_width
|
||||
continue
|
||||
|
||||
x += columns * char_width
|
||||
|
||||
# Add background rects first
|
||||
@@ -208,10 +203,6 @@ def render_terminal_svg(
|
||||
for span in spans:
|
||||
text = span["text"]
|
||||
columns = span["columns"]
|
||||
if not text:
|
||||
# Skip truly empty spans
|
||||
x += columns * char_width
|
||||
continue
|
||||
|
||||
# Build tspan attributes
|
||||
attrs = [f'x="{x:.1f}"']
|
||||
|
||||
@@ -10,6 +10,8 @@ from textual_webterm.svg_exporter import (
|
||||
_build_row_spans,
|
||||
_color_to_hex,
|
||||
_escape_xml,
|
||||
_is_box_drawing_vertical_or_corner,
|
||||
_should_break_span,
|
||||
render_terminal_svg,
|
||||
)
|
||||
|
||||
@@ -330,7 +332,61 @@ class TestBuildRowSpans:
|
||||
assert spans[1]["columns"] == 1
|
||||
|
||||
|
||||
class TestRenderTerminalSvg:
|
||||
class TestBoxDrawingHelpers:
|
||||
"""Tests for box drawing character detection helpers."""
|
||||
|
||||
def test_is_box_drawing_empty_char(self) -> None:
|
||||
"""Empty string returns False."""
|
||||
assert _is_box_drawing_vertical_or_corner("") is False
|
||||
|
||||
def test_is_box_drawing_regular_char(self) -> None:
|
||||
"""Regular ASCII characters return False."""
|
||||
assert _is_box_drawing_vertical_or_corner("A") is False
|
||||
assert _is_box_drawing_vertical_or_corner(" ") is False
|
||||
assert _is_box_drawing_vertical_or_corner("1") is False
|
||||
|
||||
def test_is_box_drawing_horizontal_lines(self) -> None:
|
||||
"""Horizontal box drawing lines return False (can merge)."""
|
||||
assert _is_box_drawing_vertical_or_corner("─") is False # U+2500
|
||||
assert _is_box_drawing_vertical_or_corner("━") is False # U+2501
|
||||
assert _is_box_drawing_vertical_or_corner("═") is False # U+2550
|
||||
|
||||
def test_is_box_drawing_vertical_lines(self) -> None:
|
||||
"""Vertical box drawing lines return True (need precise positioning)."""
|
||||
assert _is_box_drawing_vertical_or_corner("│") is True # U+2502
|
||||
assert _is_box_drawing_vertical_or_corner("┃") is True # U+2503
|
||||
assert _is_box_drawing_vertical_or_corner("║") is True # U+2551
|
||||
|
||||
def test_is_box_drawing_corners(self) -> None:
|
||||
"""Corner box drawing characters return True."""
|
||||
assert _is_box_drawing_vertical_or_corner("┌") is True
|
||||
assert _is_box_drawing_vertical_or_corner("┐") is True
|
||||
assert _is_box_drawing_vertical_or_corner("└") is True
|
||||
assert _is_box_drawing_vertical_or_corner("┘") is True
|
||||
assert _is_box_drawing_vertical_or_corner("╭") is True
|
||||
assert _is_box_drawing_vertical_or_corner("╮") is True
|
||||
assert _is_box_drawing_vertical_or_corner("╯") is True
|
||||
assert _is_box_drawing_vertical_or_corner("╰") is True
|
||||
|
||||
def test_should_break_span_empty_current(self) -> None:
|
||||
"""Empty current text never breaks."""
|
||||
assert _should_break_span("", "A") is False
|
||||
assert _should_break_span("", "│") is False
|
||||
|
||||
def test_should_break_span_normal_chars(self) -> None:
|
||||
"""Normal characters don't break spans."""
|
||||
assert _should_break_span("A", "B") is False
|
||||
assert _should_break_span("Hello", "!") is False
|
||||
|
||||
def test_should_break_span_vertical_line(self) -> None:
|
||||
"""Vertical lines cause breaks."""
|
||||
assert _should_break_span("A", "│") is True
|
||||
assert _should_break_span("│", "A") is True
|
||||
|
||||
def test_should_break_span_horizontal_lines_merge(self) -> None:
|
||||
"""Horizontal lines can merge with each other."""
|
||||
assert _should_break_span("─", "─") is False
|
||||
assert _should_break_span("━", "━") is False
|
||||
"""Tests for render_terminal_svg function."""
|
||||
|
||||
def _char(
|
||||
@@ -365,6 +421,19 @@ class TestRenderTerminalSvg:
|
||||
assert svg.endswith("</svg>")
|
||||
assert 'xmlns="http://www.w3.org/2000/svg"' in svg
|
||||
|
||||
def test_css_properties(self) -> None:
|
||||
"""SVG includes essential CSS properties for proper rendering."""
|
||||
svg = render_terminal_svg([], width=80, height=24)
|
||||
# Check for proper baseline alignment
|
||||
assert "dominant-baseline: text-before-edge" in svg
|
||||
# Check for legibility optimization
|
||||
assert "text-rendering: optimizeLegibility" in svg
|
||||
# Check for monospace font
|
||||
assert "font-family:" in svg
|
||||
assert "monospace" in svg
|
||||
# Check for pre whitespace handling
|
||||
assert "white-space: pre" in svg
|
||||
|
||||
def test_buffer_with_empty_rows(self) -> None:
|
||||
"""Buffer with rows containing only empty cells produces valid SVG."""
|
||||
# Row with only empty placeholder cells (no actual characters)
|
||||
|
||||
Reference in New Issue
Block a user