docs/mdx_spantables.py
2023-03-27 21:43:22 +01:00

187 lines
6.5 KiB
Python

"""
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 <https://pythonhosted.org/Markdown/extensions/tables.html>
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)