diff --git a/humanfriendly/terminal/html.py b/humanfriendly/terminal/html.py
index 4214e09..744972e 100644
--- a/humanfriendly/terminal/html.py
+++ b/humanfriendly/terminal/html.py
@@ -167,10 +167,12 @@ def close(self):
the input but a style is still active. This is intended to prevent
malformed HTML from messing up terminal output.
"""
- if any(self.stack):
- self.output.write(ANSI_RESET)
- self.stack = []
- HTMLParser.close(self)
+ try:
+ HTMLParser.close(self)
+ finally:
+ if any(self.stack):
+ self.output.write(ANSI_RESET)
+ self.stack = []
def emit_style(self, style=None):
"""
diff --git a/humanfriendly/tests.py b/humanfriendly/tests.py
index 72dad99..2d1dee5 100644
--- a/humanfriendly/tests.py
+++ b/humanfriendly/tests.py
@@ -1010,6 +1010,39 @@ def test_ansi_wrap(self):
# Make sure ansi_wrap() ends the text by resetting the ANSI styles.
assert ansi_wrap(text, bold=True).endswith(ANSI_RESET)
+ def test_html_buffered_tail_keeps_style(self):
+ """Flush buffered character references before closing active styles."""
+ for tag in ('b', 'em', 'code', 'span style="color: red"'):
+ for tail in ('&', '&'):
+ result = html_to_ansi('<' + tag + '>tail ' + tail)
+ self.assertTrue(result.endswith('tail &' + ANSI_RESET))
+ self.assertEqual(html_to_ansi('tail &'), 'tail &')
+
+ def test_html_streaming_close_keeps_style(self):
+ """Explicit close also styles data buffered across feed calls."""
+ from humanfriendly.terminal.html import HTMLConverter
+ converter = HTMLConverter()
+ converter.feed('tail ')
+ converter.feed('&')
+ converter.close()
+ self.assertEqual(converter.output.getvalue(), '\x1b[0m\x1b[1mtail &' + ANSI_RESET)
+ value = converter.output.getvalue()
+ converter.close()
+ self.assertEqual(converter.output.getvalue(), value)
+
+ def test_html_close_resets_style_after_callback_error(self):
+ """A callback failure during flushing still closes terminal styles."""
+ from humanfriendly.terminal.html import HTMLConverter
+
+ def callback(text):
+ raise ValueError('callback failed')
+
+ converter = HTMLConverter(callback=callback)
+ converter.feed('tail &')
+ with self.assertRaises(ValueError):
+ converter.close()
+ self.assertTrue(converter.output.getvalue().endswith(ANSI_RESET))
+
def test_html_to_ansi(self):
"""Test the :func:`humanfriendly.terminal.html_to_ansi()` function."""
assert html_to_ansi("Just some plain text") == "Just some plain text"