From 8b2c92d54f0132cd5f3f87fb1a0a734820ecc70d Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Sun, 5 Feb 2017 00:23:35 +0900 Subject: [PATCH 1/4] latex: Refactor spanning cells --- sphinx/writers/latex.py | 155 +++++++++++++++---------- tests/roots/test-latex-table/index.rst | 17 +++ tests/test_build_latex.py | 17 +++ 3 files changed, 126 insertions(+), 63 deletions(-) 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) From ee409e9b7fb6e407fc9d9cd2a682223058dab2d7 Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Tue, 7 Feb 2017 22:11:55 +0900 Subject: [PATCH 2/4] Refactor: make TableCell.__init__() simple --- sphinx/writers/latex.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/sphinx/writers/latex.py b/sphinx/writers/latex.py index 706d00e01..d03e610e3 100644 --- a/sphinx/writers/latex.py +++ b/sphinx/writers/latex.py @@ -389,12 +389,14 @@ class TableCell(object): 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 + self.row = row + self.col = col + + # adjust position for multirow/multicol cell + while table.cells[(self.row - 1, self.col)] == self.cell_id: + self.row -= 1 + while table.cells[(self.row, self.col - 1)] == self.cell_id: + self.col -= 1 @property def width(self): From 44e845de230dd9ed253e2ce7b9da4eba6ae1a24f Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Tue, 7 Feb 2017 22:51:03 +0900 Subject: [PATCH 3/4] Add comments --- sphinx/writers/latex.py | 44 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 40 insertions(+), 4 deletions(-) diff --git a/sphinx/writers/latex.py b/sphinx/writers/latex.py index d03e610e3..c5961d4b3 100644 --- a/sphinx/writers/latex.py +++ b/sphinx/writers/latex.py @@ -316,27 +316,44 @@ class ShowUrlsTransform(object): class Table(object): + """A table data""" + def __init__(self, node): # type: (nodes.table) -> None self.header = [] # type: List[unicode] 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.has_problematic = False self.has_verbatim = False self.caption = None # type: List[unicode] + + # current position + self.col = 0 + self.row = 0 + + # for internal use + self.classes = node.get('classes', []) # type: List[unicode] self.cells = defaultdict(int) # type: Dict[Tuple[int, int], int] - self.cell_id = 0 + # it maps table location to cell_id + # (cell = rectangular area) + self.cell_id = 0 # last assigned cell_id def is_longtable(self): + """True if and only if table uses longtable environment.""" # type: () -> bool return self.row > 30 or 'longtable' in self.classes def get_table_type(self): + """Returns the LaTeX environment name for the table. + + The class currently supports: + + * longtable + * tabular + * taburary + """ # type: () -> unicode if self.is_longtable(): return 'longtable' @@ -350,6 +367,12 @@ class Table(object): return 'tabulary' def get_colspec(self): + """Returns a column spec of table. + + This is what LaTeX calls the 'preamble argument' of the used table environment. + + .. note:: the ``\X`` column type specifier is defined in ``sphinx.sty``. + """ # type: () -> unicode if self.colspec: return self.colspec @@ -365,6 +388,10 @@ class Table(object): return '{|' + ('l|' * self.colcount) + '}\n' def add_cell(self, height, width): + """Adds a new cell to a table. + + It will be located at current position: (``self.row``, ``self.col``). + """ self.cell_id += 1 for col in range(width): for row in range(height): @@ -372,6 +399,11 @@ class Table(object): self.cells[(self.row + row, self.col + col)] = self.cell_id def cell(self, row=None, col=None): + """Returns a cell object (i.e. rectangular area) containing given position: (``row``, ``col``) + + If no ``row`` or ``col`` are given, the current position; ``self.row`` and + ``self.col`` are used to get a cell object by default. + """ try: if row is None: row = self.row @@ -383,6 +415,8 @@ class Table(object): class TableCell(object): + """A cell data of tables.""" + def __init__(self, table, row, col): if table.cells[(row, col)] == 0: raise IndexError @@ -400,6 +434,7 @@ class TableCell(object): @property def width(self): + """Returns the cell width.""" width = 0 while self.table.cells[(self.row, self.col + width)] == self.cell_id: width += 1 @@ -407,6 +442,7 @@ class TableCell(object): @property def height(self): + """Returns the cell height.""" height = 0 while self.table.cells[(self.row + height, self.col)] == self.cell_id: height += 1 From 141cb9e9543919d527e5ecc7b51a45e7db3bdfa0 Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Wed, 8 Feb 2017 00:38:08 +0900 Subject: [PATCH 4/4] Fix flake8 violation --- sphinx/writers/latex.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sphinx/writers/latex.py b/sphinx/writers/latex.py index c5961d4b3..6c4973f03 100644 --- a/sphinx/writers/latex.py +++ b/sphinx/writers/latex.py @@ -399,10 +399,10 @@ class Table(object): self.cells[(self.row + row, self.col + col)] = self.cell_id def cell(self, row=None, col=None): - """Returns a cell object (i.e. rectangular area) containing given position: (``row``, ``col``) + """Returns a cell object (i.e. rectangular area) containing given position. - If no ``row`` or ``col`` are given, the current position; ``self.row`` and - ``self.col`` are used to get a cell object by default. + If no option arguments: ``row`` or ``col`` are given, the current position; + ``self.row`` and ``self.col`` are used to get a cell object by default. """ try: if row is None: