Skip to content

Commit

Permalink
boxes/helper/themes: Add support for rendering tables.
Browse files Browse the repository at this point in the history
* Added support for rendering tables, in the MessageBox, using helper
  functions, parse_html_table, render_table, style_row and
  unicode_border which parse and return the table contents in a list.
* Added `table_head` in themes, for default, gruvbox, light and white,
  to specify a style for table headings.

Tests amended and added for different column alignments and types of
tables.
  • Loading branch information
preetmishra committed Apr 3, 2020
1 parent 887bad0 commit 044e64f
Show file tree
Hide file tree
Showing 4 changed files with 210 additions and 4 deletions.
59 changes: 57 additions & 2 deletions tests/ui/test_ui_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -1397,7 +1397,59 @@ 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',
'│ ', ('table_head', 'Firstname'), ' │ ',
('table_head', 'Lastname'), ' │\n',
'├─', '─────────', '─┼─', '────────', '─┤\n',
'│ ', (None, 'John '), ' │ ', (None, 'Doe '), ' │\n',
'│ ', (None, 'Mary '), ' │ ', (None, 'Moe '), ' │\n',
'└─', '─────────', '─┴─', '────────', '─┘',
]),
('<table><thead><tr><th align="left">Name</th><th align="right">Id'
'</th></tr></thead><tbody><tr><td align="left">Robert</td>'
'<td align="right">1</td></tr><tr><td align="left">Mary</td>'
'<td align="right">100</td></tr></tbody></table>', [
'┌─', '──────', '─┬─', '───', '─┐\n',
'│ ', ('table_head', 'Name '), ' │ ', ('table_head', ' Id'),
' │\n',
'├─', '──────', '─┼─', '───', '─┤\n',
'│ ', (None, 'Robert'), ' │ ', (None, ' 1'), ' │\n',
'│ ', (None, 'Mary '), ' │ ', (None, '100'), ' │\n',
'└─', '──────', '─┴─', '───', '─┘',
]),
('<table><thead><tr><th align="center">Name</th><th align="right">Id'
'</th></tr></thead><tbody><tr><td align="center">Robert</td>'
'<td align="right">1</td></tr><tr><td align="center">Mary</td>'
'<td align="right">100</td></tr></tbody></table>', [
'┌─', '──────', '─┬─', '───', '─┐\n',
'│ ', ('table_head', ' Name '), ' │ ', ('table_head', ' Id'),
' │\n',
'├─', '──────', '─┼─', '───', '─┤\n',
'│ ', (None, 'Robert'), ' │ ', (None, ' 1'), ' │\n',
'│ ', (None, ' Mary '), ' │ ', (None, '100'), ' │\n',
'└─', '──────', '─┴─', '───', '─┘',
]),
('<table><thead><tr><th>Name</th></tr></thead><tbody><tr><td>Foo</td>'
'</tr><tr><td>Bar</td></tr><tr><td>Baz</td></tr></tbody></table>', [
'┌─', '────', '─┐\n',
'│ ', ('table_head', 'Name'), ' │\n',
'├─', '────', '─┤\n',
'│ ', (None, 'Foo '), ' │\n',
'│ ', (None, 'Bar '), ' │\n',
'│ ', (None, 'Baz '), ' │\n',
'└─', '────', '─┘',
]),
('<table><thead><tr><th>Column1</th></tr></thead><tbody><tr><td></td>'
'</tr></tbody></table>', [
'┌─', '───────', '─┐\n',
'│ ', ('table_head', 'Column1'), ' │\n',
'├─', '───────', '─┤\n',
'│ ', (None, ' '), ' │\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 All @@ -1417,7 +1469,10 @@ def test_private_message_to_self(self, mocker):
'link_sametext', 'link_sameimage', 'link_differenttext',
'link_userupload', 'link_api', 'link_serverrelative_same',
'listitem', 'listitems',
'br', 'br2', 'hr', 'hr2', 'img', 'img2', 'table', 'math', 'math2',
'br', 'br2', 'hr', 'hr2', 'img', 'img2', 'table_default',
'table_with_left_and_right_alignments',
'table_with_center_and_right_alignments', 'table_with_single_column',
'table_with_the_bare_minimum', 'math', 'math2',
'ul', 'strikethrough_del', 'inline_image', 'inline_ref',
'emoji', 'preview-twitter', 'zulip_extra_emoji', 'custom_emoji'
])
Expand Down
5 changes: 5 additions & 0 deletions zulipterminal/config/themes.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
('starred', 'light red, bold', ''),
('category', 'light blue, bold', ''),
('unread_count', 'yellow', ''),
('table_head', 'white, bold', ''),
],
'gruvbox': [
# default colorscheme on 16 colors, gruvbox colorscheme
Expand Down Expand Up @@ -137,6 +138,8 @@
None, LIGHTBLUE, BLACK),
('unread_count', 'yellow', 'black',
None, YELLOW, BLACK),
('table_head', 'white, bold', 'black',
None, WHITEBOLD, BLACK),
],
'light': [
(None, 'black', 'white'),
Expand Down Expand Up @@ -166,6 +169,7 @@
('starred', 'light red, bold', 'white'),
('category', 'dark gray, bold', 'light gray'),
('unread_count', 'dark blue, bold', 'white'),
('table_head', 'black, bold', 'white'),
],
'blue': [
(None, 'black', 'light blue'),
Expand Down Expand Up @@ -195,6 +199,7 @@
('starred', 'light red, bold', 'dark blue'),
('category', 'light gray, bold', 'light blue'),
('unread_count', 'yellow', 'light blue'),
('table_head', 'black, bold', 'light blue'),
]
} # type: Dict[str, ThemeSpec]

Expand Down
145 changes: 145 additions & 0 deletions zulipterminal/helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -493,3 +493,148 @@ 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 the cell items and the column alignments.
The table cells are stored in `cells` in a row-wise manner.
cells = [[row0, row0, row0],
[row1, row1, row1],
[row2, row2, row2]]
"""
headers = table_element.thead.tr.find_all('th')
rows = table_element.tbody.find_all('tr')
column_alignments = []

# 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:
cells[0].append(header.text)
column_alignments.append(header.get(('align'), 'left'))

# 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 (column_alignments, cells)


StyledTableData = List[Union[str, Tuple[Optional[str], str]]]


def pad_row_strip(row_strip: StyledTableData, fill_char: str=' ',
fill_width: int=1) -> StyledTableData:
"""
Returns back a padded row strip.
This only pads the box-drawing unicode characters. In particular, all the
connector characters are padded both sides, the leftmost character is
padded right and the rightmost is padded left.
The structure of `row_strip` for a table with three columns:
row_strip = [
leftmost_char,
cell_content,
connector_char,
cell_content,
connector_char,
cell_content,
rightmost_char,
]
"""
fill = fill_char * fill_width

# [1] Add "type: ignore", to suppress "unsupported operand types" error,
# for row_strip[index] and fill concatenation(s) - `row_strip` is of type
# "StyledTableData" and fill is of type "str".

# Pad the leftmost box-drawing character.
row_strip[0] = row_strip[0] + fill # type: ignore # Refer to [1].

# Pad the connector box-drawing characters.
for index in range(2, len(row_strip) - 1, 2):
row_strip[index] = fill + row_strip[index] + fill # type: ignore
# Refer to [1].

# Pad the rightmost box-drawing character.
row_strip[-1] = fill + row_strip[-1] # type: ignore # Refer to [1].
return row_strip


def row_with_styled_content(row: List[str], column_alignments: List[str],
column_widths: List[int], vertical_bar: str,
row_style: Optional[str]=None) -> StyledTableData:
"""
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 = [vertical_bar, ] # type: StyledTableData
for column_num, cell in enumerate(row):
aligned_text = aligner[column_alignments[column_num]](
cell, column_widths[column_num]
)
row_strip.extend([(row_style, aligned_text), vertical_bar])
row_strip.pop() # Remove the extra vertical_bar.
row_strip.append(vertical_bar + '\n')
return pad_row_strip(row_strip)


def row_with_only_border(lcorner: str, line: str, connector: str, rcorner: str,
column_widths: List[int],
newline: bool=True) -> StyledTableData:
"""
Given left corner, line, connecter and right corner unicode character,
constructs a border row strip for markup table.
"""
border = [lcorner, ] # type: StyledTableData
for width in column_widths:
border.extend([line * width, connector])
border.pop() # Remove the extra connector.
border.append(rcorner)
if newline:
border[-1] += '\n' # type: ignore # Suppress "unsupported operand
# types" error - `border` is of type "StyledTableData" and `fill` is of
# type "str".
return pad_row_strip(border, fill_char=line)


def render_table(table_element: Any) -> StyledTableData:
"""
A helper function for rendering a markup table in the MessageBox.
"""
column_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 = row_with_only_border(u'┌', u'─', u'┬', u'┐', column_widths)
middle_border = row_with_only_border(u'├', u'─', u'┼', u'┤', column_widths)
bottom_border = row_with_only_border(u'└', u'─', u'┴', u'┘', column_widths,
newline=False)

# Construct the table, row-by-row.
table = [] # type: StyledTableData

# Add the header/0th row and the borders that surround it to the table.
table.extend(top_border)
table.extend(row_with_styled_content(cells.pop(0), column_alignments,
column_widths, u'│',
row_style='table_head'))
table.extend(middle_border)

# Add the body rows to the table followed by the bottom-most border in the
# end.
for row in cells:
table.extend(row_with_styled_content(row, column_alignments,
column_widths, u'│'))
table.extend(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 044e64f

Please sign in to comment.