Skip to content

Commit

Permalink
lf.repr_utils.html_repr to support nested HTML object.
Browse files Browse the repository at this point in the history
This allows a nested object that contains `_repr_html_` to be rendered as HTML.

PiperOrigin-RevId: 675734071
  • Loading branch information
daiyip authored and langfun authors committed Sep 17, 2024
1 parent 74e8a7e commit 67848eb
Show file tree
Hide file tree
Showing 3 changed files with 82 additions and 34 deletions.
14 changes: 12 additions & 2 deletions langfun/core/eval/matching.py
Original file line number Diff line number Diff line change
Expand Up @@ -275,9 +275,19 @@ def _render_matches(self, s: io.StringIO) -> None:
for i, (example, output, message) in enumerate(self.matches):
bgcolor = 'white' if i % 2 == 0 else '#DDDDDD'
s.write(f'<tr style="background-color: {bgcolor}"><td>{i + 1}</td>')
input_str = pg.format(example, verbose=False, max_bytes_len=32)
input_str = lf.repr_utils.escape_quoted(
pg.format(
example, verbose=False, max_bytes_len=32,
custom_format='_repr_html_'
)
)
s.write(f'<td style="color:green;white-space:pre-wrap">{input_str}</td>')
output_str = pg.format(output, verbose=False, max_bytes_len=32)
output_str = lf.repr_utils.escape_quoted(
pg.format(
output, verbose=False, max_bytes_len=32,
custom_format='_repr_html_'
)
)
s.write(f'<td style="color:blue;white-space:pre-wrap">{output_str}</td>')
s.write('<td>')
self._render_message(message, s)
Expand Down
83 changes: 53 additions & 30 deletions langfun/core/repr_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ def write_maybe_shared(s: io.StringIO, content: str) -> bool:


def html_repr(
value: Any,
value: dict[str, Any],
item_color: Callable[
[str, str],
tuple[
Expand All @@ -121,35 +121,38 @@ def html_repr(
s.write('<table style="border-top: 1px solid #EEEEEE;">')
item_color = item_color or (lambda k, v: (None, '#F1C40F', None, None))

for k, v in pg.object_utils.flatten(value).items():
if isinstance(v, pg.Ref):
v = v.value
if hasattr(v, '_repr_html_'):
cs = v._repr_html_() # pylint: disable=protected-access
else:
cs = f'<span style="white-space: pre-wrap">{html.escape(str(v))}</span>'

key_color, key_bg_color, value_color, value_bg_color = item_color(k, v)
key_span = html_round_text(
k,
text_color=key_color,
background_color=key_bg_color,
margin_bottom='0px'
)
value_color_style = f'color: {value_color};' if value_color else ''
value_bg_color_style = (
f'background-color: {value_bg_color};' if value_bg_color else ''
)
s.write(
'<tr>'
'<td style="padding: 5px; vertical-align: top; '
f'border-bottom: 1px solid #EEEEEE">{key_span}</td>'
'<td style="padding: 15px 5px 5px 5px; vertical-align: top; '
'border-bottom: 1px solid #EEEEEE;'
f'{value_color_style}{value_bg_color_style}">{cs}</td></tr>'
)
s.write('</table></div>')
return s.getvalue()
with (pg.str_format(custom_format='_repr_html_'),
pg.repr_format(custom_format='_repr_html_')):
for k, v in pg.object_utils.flatten(value).items():
if isinstance(v, pg.Ref):
v = v.value
if hasattr(v, '_repr_html_'):
cs = v._repr_html_() # pylint: disable=protected-access
else:
cs = html.escape(v) if isinstance(v, str) else escape_quoted(str(v))
cs = f'<span style="white-space: pre-wrap">{cs}</span>'

key_color, key_bg_color, value_color, value_bg_color = item_color(k, v)
key_span = html_round_text(
k,
text_color=key_color,
background_color=key_bg_color,
margin_bottom='0px'
)
value_color_style = f'color: {value_color};' if value_color else ''
value_bg_color_style = (
f'background-color: {value_bg_color};' if value_bg_color else ''
)
s.write(
'<tr>'
'<td style="padding: 5px; vertical-align: top; '
f'border-bottom: 1px solid #EEEEEE">{key_span}</td>'
'<td style="padding: 15px 5px 5px 5px; vertical-align: top; '
'border-bottom: 1px solid #EEEEEE;'
f'{value_color_style}{value_bg_color_style}">{cs}</td></tr>'
)
s.write('</table></div>')
return s.getvalue()


def html_round_text(
Expand All @@ -172,3 +175,23 @@ def html_round_text(
f'margin-top: {margin_top}; margin-bottom: {margin_bottom}; '
f'white-space: {whitespace}">{text}</span>'
)


def escape_quoted(s: str):
"""Escape quoted parts within a string."""
r = io.StringIO()
quote_char = None
quote_start = -1
for i, c in enumerate(s):
if c in ('\'', '"'):
if quote_char is None:
quote_char = c
quote_start = i
elif quote_char == c:
r.write(c)
r.write(html.escape(s[quote_start + 1:i]))
r.write(c)
quote_char = None
elif quote_char is None:
r.write(c)
return r.getvalue()
19 changes: 17 additions & 2 deletions langfun/core/repr_utils_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,15 @@ def test_sharing(self):
self.assertEqual(ctx1['<style>b</style>'], 2)
self.assertEqual(ctx1['<style>a</style>'], 4)

def test_escape_quoted(self):
self.assertEqual(
repr_utils.escape_quoted(str('<a>')), '<a>'
)
self.assertEqual(
repr_utils.escape_quoted('x=<a>, b="<a>"'),
'x=<a>, b="&lt;a&gt;"'
)

def test_html(self):
html = repr_utils.Html('<div>foo</div>')
self.assertEqual(html.content, '<div>foo</div>')
Expand All @@ -63,12 +72,18 @@ def test_html_repr(self):
class Foo(pg.Object):
x: int

class Bar(pg.Object):

def _repr_html_(self):
return '<bar>'

html = repr_utils.html_repr(
{'foo': pg.Ref(Foo(1)), 'bar': '<lf_image>'}
{'foo': pg.Ref(Foo(1)), 'bar': Bar(), 'baz': '<lf_image>'}
)
self.assertIn('foo</span>', html)
self.assertNotIn('Ref', html)
self.assertIn('<bar>', html)
self.assertIn('&lt;lf_image&gt;', html)
self.assertNotIn('Ref', html)


if __name__ == '__main__':
Expand Down

0 comments on commit 67848eb

Please sign in to comment.