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]']),
- ('', ['[TABLE NOT RENDERED]']),
+ ('Firstname | Lastname |
'
+ 'John | Doe |
Mary | Moe'
+ ' |
', [
+ '┌───────────┬──────────┐\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