diff --git a/sphinx/writers/latex.py b/sphinx/writers/latex.py index 5858ad85e..706d00e01 100644 --- a/sphinx/writers/latex.py +++ b/sphinx/writers/latex.py @@ -15,6 +15,7 @@ import re import sys from os import path +from collections import defaultdict from six import itervalues, text_type from docutils import nodes, writers @@ -321,17 +322,19 @@ class Table(object): self.body = [] # type: List[unicode] self.classes = node.get('classes', []) # type: List[unicode] self.col = 0 + self.row = 0 self.colcount = 0 self.colspec = None # type: unicode self.colwidths = [] # type: List[int] - self.rowcount = 0 self.has_problematic = False self.has_verbatim = False self.caption = None # type: List[unicode] + self.cells = defaultdict(int) # type: Dict[Tuple[int, int], int] + self.cell_id = 0 def is_longtable(self): # type: () -> bool - return self.rowcount > 30 or 'longtable' in self.classes + return self.row > 30 or 'longtable' in self.classes def get_table_type(self): # type: () -> unicode @@ -361,6 +364,52 @@ class Table(object): else: return '{|' + ('l|' * self.colcount) + '}\n' + def add_cell(self, height, width): + self.cell_id += 1 + for col in range(width): + for row in range(height): + assert self.cells[(self.row + row, self.col + col)] == 0 + self.cells[(self.row + row, self.col + col)] = self.cell_id + + def cell(self, row=None, col=None): + try: + if row is None: + row = self.row + if col is None: + col = self.col + return TableCell(self, row, col) + except IndexError: + return None + + +class TableCell(object): + def __init__(self, table, row, col): + if table.cells[(row, col)] == 0: + raise IndexError + + self.table = table + self.cell_id = table.cells[(row, col)] + for n in range(row + 1): + if table.cells[(row - n, col)] == self.cell_id: + self.row = row - n + for n in range(col + 1): + if table.cells[(row, col - n)] == self.cell_id: + self.col = col - n + + @property + def width(self): + width = 0 + while self.table.cells[(self.row, self.col + width)] == self.cell_id: + width += 1 + return width + + @property + def height(self): + height = 0 + while self.table.cells[(self.row + height, self.col)] == self.cell_id: + height += 1 + return height + def escape_abbr(text): # type: (unicode) -> unicode @@ -417,8 +466,6 @@ class LaTeXTranslator(nodes.NodeVisitor): self.in_parsed_literal = 0 self.compact_list = 0 self.first_param = 0 - self.remember_multirow = {} # type: Dict[int, int] - self.remember_multirowcol = {} # type: Dict[int, int] # determine top section level if builder.config.latex_toplevel_sectioning: @@ -1254,72 +1301,53 @@ class LaTeXTranslator(nodes.NodeVisitor): def depart_tbody(self, node): # type: (nodes.Node) -> None self.popbody() - self.remember_multirow = {} - self.remember_multirowcol = {} def visit_row(self, node): # type: (nodes.Node) -> None self.table.col = 0 - for key, value in self.remember_multirow.items(): - if not value and key in self.remember_multirowcol: - del self.remember_multirowcol[key] + + # fill column if first one is a wide-multirow + cell = self.table.cell(self.table.row, 0) + if cell and cell.row != self.table.row: # bottom part of multirow cell + self.table.col += cell.width + if cell.width > 1: # use \multicolumn for wide multirow cell + self.body.append('\\multicolumn{%d}{|l|}{}\\relax ' % cell.width) def depart_row(self, node): # type: (nodes.Node) -> None self.body.append('\\\\\n') - if any(self.remember_multirow.values()): - linestart = 1 - col = self.table.colcount - for col in range(1, self.table.col + 1): - if self.remember_multirow.get(col): - if linestart != col: - linerange = str(linestart) + '-' + str(col - 1) - self.body.append('\\cline{' + linerange + '}') - linestart = col + 1 - if self.remember_multirowcol.get(col, 0): - linestart += self.remember_multirowcol[col] - if linestart <= col: - linerange = str(linestart) + '-' + str(col) - self.body.append('\\cline{' + linerange + '}') - else: + cells = [self.table.cell(self.table.row, i) for i in range(self.table.colcount)] + underlined = [cell.row + cell.height == self.table.row + 1 for cell in cells] + if all(underlined): self.body.append('\\hline') - self.table.rowcount += 1 + else: + i = 0 + underlined.extend([False]) # sentinel + while i < len(underlined): + if underlined[i] is True: + j = underlined[i:].index(False) + self.body.append('\\cline{%d-%d}' % (i + 1, i + j)) + i += j + i += 1 + self.table.row += 1 def visit_entry(self, node): # type: (nodes.Node) -> None - if self.table.col == 0: - while self.remember_multirow.get(self.table.col + 1, 0): - self.table.col += 1 - self.remember_multirow[self.table.col] -= 1 - if self.remember_multirowcol.get(self.table.col, 0): - extracols = self.remember_multirowcol[self.table.col] - self.body.append('\\multicolumn{') - self.body.append(str(extracols + 1)) - self.body.append('}{|l|}{}\\relax ') - self.table.col += extracols - self.body.append('&') - else: + if self.table.col > 0: self.body.append('&') - self.table.col += 1 + self.table.add_cell(node.get('morerows', 0) + 1, node.get('morecols', 0) + 1) + cell = self.table.cell() context = '' - if 'morecols' in node: - self.body.append('\\multicolumn{') - self.body.append(str(node.get('morecols') + 1)) - if self.table.col == 1: - self.body.append('}{|l|}{\\relax ') + if cell.width > 1: + self.body.append('\\multicolumn{%d}' % cell.width) + if self.table.col == 0: + self.body.append('{|l|}{\\relax ') else: - self.body.append('}{l|}{\\relax ') + self.body.append('{l|}{\\relax ') context += '\\unskip}\\relax ' - if 'morerows' in node: - self.body.append('\\multirow{') - self.body.append(str(node.get('morerows') + 1)) - self.body.append('}{*}{\\relax ') + if cell.height > 1: + self.body.append('\\multirow{%d}{*}{\\relax ' % cell.height) context += '\\unskip}\\relax ' - self.remember_multirow[self.table.col] = node.get('morerows') - if 'morecols' in node: - if 'morerows' in node: - self.remember_multirowcol[self.table.col] = node.get('morecols') - self.table.col += node.get('morecols') if (('morecols' in node or 'morerows' in node) and (len(node) > 2 or len(node.astext().split('\n')) > 2)): self.in_merged_cell = 1 @@ -1333,16 +1361,6 @@ class LaTeXTranslator(nodes.NodeVisitor): else: self.body.append('\\sphinxstylethead{\\relax ') context += '\\unskip}\\relax ' - while self.remember_multirow.get(self.table.col + 1, 0): - self.table.col += 1 - self.remember_multirow[self.table.col] -= 1 - context += '&' - if self.remember_multirowcol.get(self.table.col, 0): - extracols = self.remember_multirowcol[self.table.col] - context += '\\multicolumn{' - context += str(extracols + 1) - context += '}{l|}{}\\relax ' - self.table.col += extracols if len(node.traverse(nodes.paragraph)) >= 2: self.table.has_problematic = True self.context.append(context) @@ -1361,6 +1379,17 @@ class LaTeXTranslator(nodes.NodeVisitor): self.body.append(line) self.body.append(self.context.pop()) # header + cell = self.table.cell() + self.table.col += cell.width + + # fill column if next one is a wide-multirow + nextcell = self.table.cell() + if nextcell and nextcell.row != self.table.row: # bottom part of multirow cell + self.table.col += nextcell.width + self.body.append('&') + if nextcell.width > 1: # use \multicolumn for wide multirow cell + self.body.append('\\multicolumn{%d}{l|}{}\\relax ' % nextcell.width) + def visit_acks(self, node): # type: (nodes.Node) -> None # this is a list in the source, but should be rendered as a diff --git a/tests/roots/test-latex-table/index.rst b/tests/roots/test-latex-table/index.rst index ae461df2d..129d024a0 100644 --- a/tests/roots/test-latex-table/index.rst +++ b/tests/roots/test-latex-table/index.rst @@ -12,6 +12,23 @@ cell2-1 cell2-2 cell3-1 cell3-2 ======= ======= +grid table +---------- + ++---------+---------+---------+ +| header1 | header2 | header3 | ++=========+=========+=========+ +| cell1-1 | cell1-2 | cell1-3 | ++---------+ +---------+ +| cell2-1 | | cell2-2 | ++ +---------+---------+ +| | cell3-2 | ++---------+ | +| cell4-1 | | ++---------+---------+---------+ +| cell5-1 | ++---------+---------+---------+ + table having :widths: option ---------------------------- diff --git a/tests/test_build_latex.py b/tests/test_build_latex.py index 1c1cd3928..17fa61957 100644 --- a/tests/test_build_latex.py +++ b/tests/test_build_latex.py @@ -839,6 +839,23 @@ def test_latex_table(app, status, warning): assert ('\\hline\ncell3-1\n&\ncell3-2\n\\\\' in table) assert ('\\hline\n\\end{tabulary}' in table) + # grid table + table = tables['grid table'] + assert ('\\noindent\\begin{tabulary}{\\linewidth}{|L|L|L|}' in table) + assert ('\\hline\n' + '\\sphinxstylethead{\\relax \nheader1\n\\unskip}\\relax &' + '\\sphinxstylethead{\\relax \nheader2\n\\unskip}\\relax &' + '\\sphinxstylethead{\\relax \nheader3\n\\unskip}\\relax \\\\' in table) + assert ('\\hline\ncell1-1\n&\\multirow{2}{*}{\\relax \ncell1-2\n\\unskip}\\relax &\n' + 'cell1-3\n\\\\' in table) + assert ('\\cline{1-1}\\cline{3-3}\\multirow{2}{*}{\\relax \ncell2-1\n\\unskip}\\relax &&\n' + 'cell2-2\n\\\\' in table) + assert ('\\cline{2-3}&\\multicolumn{2}{l|}{\\relax \\multirow{2}{*}{\\relax \n' + 'cell3-2\n\\unskip}\\relax \\unskip}\\relax \\\\' in table) + assert ('\\cline{1-1}\ncell4-1\n&\\multicolumn{2}{l|}{}\\relax \\\\' in table) + assert ('\\hline\\multicolumn{3}{|l|}{\\relax \ncell5-1\n\\unskip}\\relax \\\\\n' + '\\hline\n\\end{tabulary}' in table) + # table having :widths: option table = tables['table having :widths: option'] assert ('\\noindent\\begin{tabular}{|\\X{30}{100}|\\X{70}{100}|}' in table)