Fix horizontal box-drawing alignment with textLength attribute
Horizontal line characters (─━═) render narrower than the intended character width in most fonts, causing gaps when followed by other characters. Now using textLength + lengthAdjust='spacing' to force horizontal box-drawing spans to occupy their correct width. - Added _is_all_horizontal_box_drawing() helper - Added textLength attribute for horizontal line spans > 1 char - Added comprehensive tests for new functionality - svg_exporter.py now has 100% test coverage Version bump to 0.3.8
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "textual-webterm"
|
name = "textual-webterm"
|
||||||
version = "0.3.7"
|
version = "0.3.8"
|
||||||
description = "Serve terminal sessions over the web"
|
description = "Serve terminal sessions over the web"
|
||||||
authors = ["Will McGugan <will@textualize.io>"]
|
authors = ["Will McGugan <will@textualize.io>"]
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|||||||
@@ -222,6 +222,13 @@ def render_terminal_svg(
|
|||||||
if classes:
|
if classes:
|
||||||
attrs.append(f'class="{" ".join(classes)}"')
|
attrs.append(f'class="{" ".join(classes)}"')
|
||||||
|
|
||||||
|
# For horizontal box-drawing spans, use textLength to ensure correct width
|
||||||
|
# This prevents gaps caused by font rendering of ─ being narrower than char_width
|
||||||
|
if _is_all_horizontal_box_drawing(text) and len(text) > 1:
|
||||||
|
span_width = columns * char_width
|
||||||
|
attrs.append(f'textLength="{span_width:.1f}"')
|
||||||
|
attrs.append('lengthAdjust="spacing"')
|
||||||
|
|
||||||
parts.append(f'<tspan {" ".join(attrs)}>{_escape_xml(text)}</tspan>')
|
parts.append(f'<tspan {" ".join(attrs)}>{_escape_xml(text)}</tspan>')
|
||||||
x += columns * char_width
|
x += columns * char_width
|
||||||
|
|
||||||
@@ -274,6 +281,23 @@ def _is_box_drawing_vertical_or_corner(char: str) -> bool:
|
|||||||
return code not in horizontal_chars
|
return code not in horizontal_chars
|
||||||
|
|
||||||
|
|
||||||
|
# Set of horizontal box-drawing character codes for span detection
|
||||||
|
_HORIZONTAL_BOX_CHARS = {
|
||||||
|
0x2500, 0x2501, # ─ ━
|
||||||
|
0x2504, 0x2505, # ┄ ┅ (horizontal dashed)
|
||||||
|
0x2508, 0x2509, # ┈ ┉ (horizontal dashed)
|
||||||
|
0x254C, 0x254D, # ╌ ╍ (horizontal dashed)
|
||||||
|
0x2550, # ═
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _is_all_horizontal_box_drawing(text: str) -> bool:
|
||||||
|
"""Check if text consists entirely of horizontal box-drawing characters."""
|
||||||
|
if not text:
|
||||||
|
return False
|
||||||
|
return all(ord(c) in _HORIZONTAL_BOX_CHARS for c in text)
|
||||||
|
|
||||||
|
|
||||||
def _should_break_span(current_text: str, new_char: str) -> bool:
|
def _should_break_span(current_text: str, new_char: str) -> bool:
|
||||||
"""Check if we should break the span before adding new_char.
|
"""Check if we should break the span before adding new_char.
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ from textual_webterm.svg_exporter import (
|
|||||||
_build_row_spans,
|
_build_row_spans,
|
||||||
_color_to_hex,
|
_color_to_hex,
|
||||||
_escape_xml,
|
_escape_xml,
|
||||||
|
_is_all_horizontal_box_drawing,
|
||||||
_is_box_drawing_vertical_or_corner,
|
_is_box_drawing_vertical_or_corner,
|
||||||
_should_break_span,
|
_should_break_span,
|
||||||
render_terminal_svg,
|
render_terminal_svg,
|
||||||
@@ -387,6 +388,30 @@ class TestBoxDrawingHelpers:
|
|||||||
"""Horizontal lines can merge with each other."""
|
"""Horizontal lines can merge with each other."""
|
||||||
assert _should_break_span("─", "─") is False
|
assert _should_break_span("─", "─") is False
|
||||||
assert _should_break_span("━", "━") is False
|
assert _should_break_span("━", "━") is False
|
||||||
|
|
||||||
|
def test_is_all_horizontal_box_drawing_empty(self) -> None:
|
||||||
|
"""Empty string returns False."""
|
||||||
|
assert _is_all_horizontal_box_drawing("") is False
|
||||||
|
|
||||||
|
def test_is_all_horizontal_box_drawing_normal_text(self) -> None:
|
||||||
|
"""Normal text returns False."""
|
||||||
|
assert _is_all_horizontal_box_drawing("Hello") is False
|
||||||
|
assert _is_all_horizontal_box_drawing("ABC") is False
|
||||||
|
|
||||||
|
def test_is_all_horizontal_box_drawing_horizontal_lines(self) -> None:
|
||||||
|
"""Horizontal box chars return True."""
|
||||||
|
assert _is_all_horizontal_box_drawing("─") is True
|
||||||
|
assert _is_all_horizontal_box_drawing("───") is True
|
||||||
|
assert _is_all_horizontal_box_drawing("━━━") is True
|
||||||
|
assert _is_all_horizontal_box_drawing("═══") is True
|
||||||
|
|
||||||
|
def test_is_all_horizontal_box_drawing_mixed(self) -> None:
|
||||||
|
"""Mixed content returns False."""
|
||||||
|
assert _is_all_horizontal_box_drawing("─A─") is False
|
||||||
|
assert _is_all_horizontal_box_drawing("│──") is False # vertical at start
|
||||||
|
|
||||||
|
|
||||||
|
class TestRenderTerminalSvg:
|
||||||
"""Tests for render_terminal_svg function."""
|
"""Tests for render_terminal_svg function."""
|
||||||
|
|
||||||
def _char(
|
def _char(
|
||||||
@@ -797,6 +822,20 @@ class TestEdgeCases:
|
|||||||
assert "─" in svg
|
assert "─" in svg
|
||||||
assert "┐" in svg
|
assert "┐" in svg
|
||||||
|
|
||||||
|
def test_horizontal_lines_use_textlength(self) -> None:
|
||||||
|
"""Horizontal line spans use textLength for correct width."""
|
||||||
|
buffer = [[
|
||||||
|
self._char("╭"),
|
||||||
|
self._char("─"),
|
||||||
|
self._char("─"),
|
||||||
|
self._char("─"),
|
||||||
|
self._char("╮"),
|
||||||
|
]]
|
||||||
|
svg = render_terminal_svg(buffer, width=5, height=1)
|
||||||
|
# Horizontal lines should have textLength attribute
|
||||||
|
assert 'textLength="24.0"' in svg
|
||||||
|
assert 'lengthAdjust="spacing"' in svg
|
||||||
|
|
||||||
def test_ansi_bright_colors(self) -> None:
|
def test_ansi_bright_colors(self) -> None:
|
||||||
"""All bright ANSI colors render."""
|
"""All bright ANSI colors render."""
|
||||||
colors = ["brightred", "brightgreen", "brightyellow",
|
colors = ["brightred", "brightgreen", "brightyellow",
|
||||||
|
|||||||
Reference in New Issue
Block a user