Skip to content

Commit

Permalink
boxes/helper: Add support for rendering tables.
Browse files Browse the repository at this point in the history
This adds support for rendering tables, in the MessageBox, by using
helper functions, parse_html_table, render_table, style_row and
unicode_border that together parse and return the table contents as a
list of strings.

Tests amended.
  • Loading branch information
preetmishra committed Mar 30, 2020
1 parent 3ac7991 commit 5b218a3
Show file tree
Hide file tree
Showing 3 changed files with 104 additions and 3 deletions.
10 changes: 9 additions & 1 deletion tests/ui/test_ui_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -1397,7 +1397,15 @@ def test_private_message_to_self(self, mocker):
('<hr/>', ['[RULER NOT RENDERED]']),
('<img>', ['[IMAGE NOT RENDERED]']),
('<img/>', ['[IMAGE NOT RENDERED]']),
('<table>stuff</table>', ['[TABLE NOT RENDERED]']),
('<table><thead><tr><th>Firstname</th><th>Lastname</th></tr></thead>'
'<tbody><tr><td>John</td><td>Doe</td></tr><tr><td>Mary</td><td>Moe'
'</td></tr></tbody></table>', [
'┌───────────┬──────────┐\n',
('bold', '│ Firstname │ Lastname │\n'),
'├───────────┼──────────┤\n',
'│ John │ Doe │\n',
'│ Mary │ Moe │\n',
'└───────────┴──────────┘']),
('<span class="katex-display">some-math</span>', ['some-math']),
('<span class="katex">some-math</span>', ['some-math']),
('<ul><li>text</li></ul>', ['', ' * ', '', 'text']),
Expand Down
92 changes: 92 additions & 0 deletions zulipterminal/helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -493,3 +493,95 @@ def notify(title: str, html_text: str) -> None:
if command:
res = subprocess.run(shlex.split(command), stdout=subprocess.DEVNULL,
stderr=subprocess.STDOUT)


def parse_html_table(table_element: Any) -> Tuple[List[str], List[List[str]]]:
"""
Parses an HTML table to extract cell items and column alignments.
"""
headers = table_element.thead.tr.find_all('th')
rows = table_element.tbody.find_all('tr')
alignments = []

# The table cells are stored row-wise:
# [[row0, row0, row0],
# [row1, row1, row1],
# [row2, row2, row2]].
# Add +1 to count the header row as well.
cells = [[] for _ in range(len(rows) + 1)] # type: List[List[str]]

# Fill up the `cells` with the header/0th row and extract alignments.
for header in headers:
alignments.append(header.get(('align'), 'left'))
cells[0].append(header.text)

# Fill up the `cells` with body rows.
for index, row in enumerate(rows, start=1):
for tdata in row.find_all('td'):
cells[index].append(tdata.text)
return (alignments, cells)


def unicode_border(lcorner: str, line: str, connector: str, rcorner: str,
widths: List[int], newline: bool=True) -> str:
"""
Given left corner, line, connecter and right corner unicode character,
constructs border for markup table.
"""
border = lcorner
for width in widths:
# Add +2 to the width to compensate for the extra space added around
# each string while styling the row by `style_row`.
border += (line * (width + 2)) + connector
border = border.rstrip(connector) + rcorner
return border + '\n' if newline else border


def style_row(alignments: List[str], column_widths: List[int],
row: List[str]) -> str:
"""
Constructs styled row strip, for markup table, using unicode characters
and row elements.
"""
aligner = {'center': str.center, 'left': str.ljust, 'right': str.rjust}
row_strip = u'│ '
for column_num, cell in enumerate(row):
aligned_text = aligner[alignments[column_num]](
cell, column_widths[column_num]
)
row_strip += (aligned_text + u' │ ')
row_strip = row_strip.rstrip() + '\n'
return row_strip


def render_table(table_element: Any) -> List[Union[str, Tuple[str, str]]]:
"""
A helper function for rendering a markup table in the MessageBox.
"""
alignments, cells = parse_html_table(table_element)

# Calculate the width required for each column.
column_widths = [
len(max(column, key=lambda string: len(string)))
for column in zip(*cells)
]

top_border = unicode_border(u'┌', u'─', u'┬', u'┐', column_widths)
middle_border = unicode_border(u'├', u'─', u'┼', u'┤', column_widths)
bottom_border = unicode_border(u'└', u'─', u'┴', u'┘', column_widths,
newline=False)

# Construct the table, row-by-row.
table = [] # type: List[Union[str, Tuple[str, str]]]
for row_num, row in enumerate(cells):
styled_row = style_row(alignments, column_widths, row)
if row_num == 0:
table.append(top_border)
# Make the header bold.
table.append(('bold', styled_row))
table.append(middle_border)
else:
table.append(styled_row)
table.append(bottom_border)

return table
5 changes: 3 additions & 2 deletions zulipterminal/ui_tools/boxes.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from zulipterminal.config.keys import is_command_key, keys_for_command
from zulipterminal.emoji_names import EMOJI_NAMES
from zulipterminal.helper import (
Message, match_emoji, match_groups, match_stream, match_user,
Message, match_emoji, match_groups, match_stream, match_user, render_table,
)
from zulipterminal.urwid_types import urwid_Size

Expand Down Expand Up @@ -400,7 +400,6 @@ def soup2markup(self, soup: Any) -> List[Any]:
'br': '', # No indicator of absence
'hr': 'RULER',
'img': 'IMAGE',
'table': 'TABLE'
}
unrendered_div_classes = { # In pairs of 'div_class': 'text'
# TODO: Support embedded content & twitter preview?
Expand Down Expand Up @@ -496,6 +495,8 @@ def soup2markup(self, soup: Any) -> List[Any]:
# TODO: Support nested lists
markup.append(' * ')
markup.extend(self.soup2markup(element))
elif element.name == 'table':
markup.extend(render_table(element))
else:
markup.extend(self.soup2markup(element))
return markup
Expand Down

0 comments on commit 5b218a3

Please sign in to comment.