diff --git a/tests/ui/test_ui_tools.py b/tests/ui/test_ui_tools.py index 40832fba0d4..9843bfb8a01 100644 --- a/tests/ui/test_ui_tools.py +++ b/tests/ui/test_ui_tools.py @@ -1397,7 +1397,15 @@ def test_private_message_to_self(self, mocker): ('
', ['[RULER NOT RENDERED]']), ('', ['[IMAGE NOT RENDERED]']), ('', ['[IMAGE NOT RENDERED]']), - ('stuff
', ['[TABLE NOT RENDERED]']), + ('' + '
FirstnameLastname
JohnDoe
MaryMoe' + '
', [ + '┌───────────┬──────────┐\n', + ('bold', '│ Firstname │ Lastname │\n'), + '├───────────┼──────────┤\n', + '│ John │ Doe │\n', + '│ Mary │ Moe │\n', + '└───────────┴──────────┘']), ('some-math', ['some-math']), ('some-math', ['some-math']), ('', ['', ' * ', '', 'text']), diff --git a/zulipterminal/helper.py b/zulipterminal/helper.py index e226451406d..0b2d781d29f 100644 --- a/zulipterminal/helper.py +++ b/zulipterminal/helper.py @@ -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