""" SpanTables Extension for Python-Markdown ======================================== This is a slightly modified version of the tables extension that comes with python-markdown. To span cells across multiple columns make sure the cells end with multiple consecutive vertical bars. To span cells across rows fill the cell on the last row with at least one underscore at the start or end of its content and no other characters than spaces or underscores. For example: | head1 | head2 | |-----------------|-------| | span two cols || | span two rows | | |_ | | See for documentation of the original extension. Original code Copyright 2009 [Waylan Limberg](http://achinghead.com) SpanTables changes Copyright 2016 [Maurice van der Pot](griffon26@kfk4ever.com) License: [BSD](http://www.opensource.org/licenses/bsd-license.php) """ from __future__ import unicode_literals from markdown.extensions.tables import TableProcessor from markdown.extensions import Extension import xml.etree.ElementTree as etree class SpanTableProcessor(TableProcessor): """ Process Tables. """ def test(self, parent, block): rows = block.split('\n') return (len(rows) > 1 and '|' in rows[0] and '|' in rows[1] and '-' in rows[1] and rows[1].strip()[0] in ['|', ':', '-']) def is_end_of_rowspan(self, td): return ((td is not None) and (td.text.startswith('^') or td.text.endswith('^')) and (td.text.strip('^ ') == '')) def apply_rowspans(self, tbody): table_cells = {} rows = tbody.findall('tr') max_cols = 0 max_rows = len(rows) for y, tr in enumerate(rows): cols = tr.findall('td') x = 0 for td in cols: colspan_str = td.get('colspan') colspan = int(colspan_str) if colspan_str else 1 # Insert the td together with its parent table_cells[(x, y)] = (tr, td) x += colspan max_cols = max(max_cols, x) for x in range(max_cols): possible_cells_in_rowspan = 0 current_colspan = None for y in range(max_rows): _, td = table_cells.get((x, y), (None, None)) if td is None: possible_cells_in_rowspan = 0 else: colspan = td.get('colspan') if colspan != current_colspan: current_colspan = colspan possible_cells_in_rowspan = 0 if not td.text: possible_cells_in_rowspan += 1 elif self.is_end_of_rowspan(td): td.text = '' possible_cells_in_rowspan += 1 first_cell_of_rowspan_y = y - (possible_cells_in_rowspan - 1) for del_y in range(y, first_cell_of_rowspan_y, -1): tr, td = table_cells.get((x, del_y)) tr.remove(td) _, first_cell = table_cells.get((x, first_cell_of_rowspan_y)) first_cell.set('rowspan', str(possible_cells_in_rowspan)) possible_cells_in_rowspan = 0 else: possible_cells_in_rowspan = 1 def run(self, parent, blocks): """ Parse a table block and build table. """ block = blocks.pop(0).split('\n') header = block[0].strip() seperator = block[1].strip() rows = [] if len(block) < 3 else block[2:] # Get format type (bordered by pipes or not) border = False if header.startswith('|'): border = True # Get alignment of columns align = [] for c in self._split_row(seperator, border): if c.startswith(':') and c.endswith(':'): align.append('center') elif c.startswith(':'): align.append('left') elif c.endswith(':'): align.append('right') else: align.append(None) # Build table table = etree.SubElement(parent, 'table') thead = etree.SubElement(table, 'thead') self._build_row(header, thead, align, border) tbody = etree.SubElement(table, 'tbody') for row in rows: self._build_row(row.strip(), tbody, align, border) self.apply_rowspans(tbody) def _build_row(self, row, parent, align, border): """ Given a row of text, build table cells. """ tr = etree.SubElement(parent, 'tr') tag = 'td' if parent.tag == 'thead': tag = 'th' cells = self._split_row(row, border) c = None # We use align here rather than cells to ensure every row # contains the same number of columns. for i, a in enumerate(align): # After this None indicates that the cell before it should span # this column and '' indicates an cell without content try: text = cells[i] if text == '': text = None except IndexError: # pragma: no cover text = '' # No text after split indicates colspan if text is None or text.strip() == "<": if c is not None: colspan_str = c.get('colspan') colspan = int(colspan_str) if colspan_str else 1 c.set('colspan', str(colspan + 1)) else: # if this is the first cell, then fall back to creating an empty cell text = '' else: c = etree.SubElement(tr, tag) c.text = text.strip() if a: c.set('align', a) def _split_row(self, row, border): """ split a row of text into list of cells. """ if border: if row.startswith('|'): row = row[1:] if row.endswith('|'): row = row[:-1] return self._split(row) class TableExtension(Extension): """ Add tables to Markdown. """ def extendMarkdown(self, md): """ Add an instance of SpanTableProcessor to BlockParser. """ md.parser.blockprocessors.register(SpanTableProcessor(md.parser, {}), 'spantable', 1000) def makeExtension(*args, **kwargs): return TableExtension(*args, **kwargs)