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"