Skip to content

Commit

Permalink
Close sphinx-doc#6740: LaTeX: Support tables without column rules
Browse files Browse the repository at this point in the history
  • Loading branch information
tk0miya committed May 3, 2020
1 parent dc45b82 commit 05a5f30
Show file tree
Hide file tree
Showing 7 changed files with 212 additions and 15 deletions.
2 changes: 2 additions & 0 deletions CHANGES
Expand Up @@ -34,6 +34,8 @@ Features added
* LaTeX: Make the ``toplevel_sectioning`` setting optional in LaTeX theme
* LaTeX: Allow to override papersize and pointsize from LaTeX themes
* LaTeX: Add :confval:`latex_theme_options` to override theme options
* #6740: LaTeX: Support tables without column rules via a theme option:
``show_table_column_rules``
* #7410: Allow to suppress "circular toctree references detected" warnings using
:confval:`suppress_warnings`
* C, added scope control directives, :rst:dir:`c:namespace`,
Expand Down
21 changes: 15 additions & 6 deletions sphinx/builders/latex/theming.py
Expand Up @@ -10,7 +10,7 @@

import configparser
from os import path
from typing import Dict
from typing import Any, Dict

from sphinx.application import Sphinx
from sphinx.config import Config
Expand All @@ -25,14 +25,15 @@ class Theme:
"""A set of LaTeX configurations."""

LATEX_ELEMENTS_KEYS = ['papersize', 'pointsize']
UPDATABLE_KEYS = ['papersize', 'pointsize']
UPDATABLE_KEYS = ['papersize', 'pointsize', 'show_table_column_rules']

def __init__(self, name: str) -> None:
self.name = name
self.docclass = name
self.wrapperclass = name
self.papersize = 'letterpaper'
self.pointsize = '10pt'
self.show_table_column_rules = True
self.toplevel_sectioning = 'chapter'

def update(self, config: Config) -> None:
Expand Down Expand Up @@ -76,7 +77,12 @@ class UserTheme(Theme):
"""A user defined LaTeX theme."""

REQUIRED_CONFIG_KEYS = ['docclass', 'wrapperclass']
OPTIONAL_CONFIG_KEYS = ['papersize', 'pointsize', 'toplevel_sectioning']
OPTIONAL_CONFIG_KEYS = {
'papersize': str,
'pointsize': str,
'toplevel_sectioning': str,
'show_table_column_rules': bool,
}

def __init__(self, name: str, filename: str) -> None:
super().__init__(name)
Expand All @@ -85,16 +91,19 @@ def __init__(self, name: str, filename: str) -> None:

for key in self.REQUIRED_CONFIG_KEYS:
try:
value = self.config.get('theme', key)
value = self.config.get('theme', key) # type: Any
setattr(self, key, value)
except configparser.NoSectionError:
raise ThemeError(__('%r doesn\'t have "theme" setting') % filename)
except configparser.NoOptionError as exc:
raise ThemeError(__('%r doesn\'t have "%s" setting') % (filename, exc.args[0]))

for key in self.OPTIONAL_CONFIG_KEYS:
for key, typ in self.OPTIONAL_CONFIG_KEYS.items():
try:
value = self.config.get('theme', key)
if typ is bool:
value = self.config.getboolean('theme', key)
else: # str
value = self.config.get('theme', key)
setattr(self, key, value)
except configparser.NoOptionError:
pass
Expand Down
41 changes: 32 additions & 9 deletions sphinx/writers/latex.py
Expand Up @@ -108,7 +108,7 @@ def translate(self) -> None:
class Table:
"""A table data"""

def __init__(self, node: Element) -> None:
def __init__(self, node: Element, **options: Any) -> None:
self.header = [] # type: List[str]
self.body = [] # type: List[str]
self.align = node.get('align')
Expand All @@ -131,6 +131,7 @@ def __init__(self, node: Element) -> None:
# it maps table location to cell_id
# (cell = rectangular area)
self.cell_id = 0 # last assigned cell_id
self.options = options

def is_longtable(self) -> bool:
"""True if and only if table uses longtable environment."""
Expand Down Expand Up @@ -178,7 +179,10 @@ def get_colspec(self) -> str:
else:
colspecs = ['l'] * self.colcount

return '{|%s|}\n' % '|'.join(colspecs)
if self.options.get('column_rules', True):
return '{|%s|}\n' % '|'.join(colspecs)
else:
return '{%s}\n' % ''.join(colspecs)

def add_cell(self, height: int, width: int) -> None:
"""Adds a new cell to a table.
Expand Down Expand Up @@ -873,7 +877,10 @@ def visit_table(self, node: Element) -> None:
'%s:%s: deeply nested tables are not implemented.' %
(self.curfilestack[-1], node.line or ''))

self.tables.append(Table(node))
table_options = {
'column_rules': self.theme.show_table_column_rules,
}
self.tables.append(Table(node, **table_options))
if self.next_table_colspec:
self.table.colspec = '{%s}\n' % self.next_table_colspec
if 'colwidths-given' in node.get('classes', []):
Expand Down Expand Up @@ -938,9 +945,14 @@ def visit_row(self, node: Element) -> None:
# insert suitable strut for equalizing row heights in given multirow
self.body.append('\\sphinxtablestrut{%d}' % cell.cell_id)
else: # use \multicolumn for wide multirow cell
self.body.append('\\multicolumn{%d}{|l|}'
if self.theme.show_table_column_rules:
colspec = '|l|'
else:
colspec = 'l'

self.body.append('\\multicolumn{%d}{%s}'
'{\\sphinxtablestrut{%d}}' %
(cell.width, cell.cell_id))
(cell.width, colspec, cell.cell_id))

def depart_row(self, node: Element) -> None:
self.body.append('\\\\\n')
Expand Down Expand Up @@ -968,9 +980,15 @@ def visit_entry(self, node: Element) -> None:
if cell.width > 1:
if self.builder.config.latex_use_latex_multicolumn:
if self.table.col == 0:
self.body.append('\\multicolumn{%d}{|l|}{%%\n' % cell.width)
if self.theme.show_table_column_rules:
self.body.append('\\multicolumn{%d}{|l|}{%%\n' % cell.width)
else:
self.body.append('\\multicolumn{%d}{l}{%%\n' % cell.width)
else:
self.body.append('\\multicolumn{%d}{l|}{%%\n' % cell.width)
if self.theme.show_table_column_rules:
self.body.append('\\multicolumn{%d}{l|}{%%\n' % cell.width)
else:
self.body.append('\\multicolumn{%d}{l}{%%\n' % cell.width)
context = '}%\n'
else:
self.body.append('\\sphinxstartmulticolumn{%d}%%\n' % cell.width)
Expand Down Expand Up @@ -1025,9 +1043,14 @@ def depart_entry(self, node: Element) -> None:
self.body.append('\\sphinxtablestrut{%d}' % nextcell.cell_id)
else:
# use \multicolumn for wide multirow cell
self.body.append('\\multicolumn{%d}{l|}'
if self.theme.show_table_column_rules:
colspec = 'l|'
else:
colspec = 'l'

self.body.append('\\multicolumn{%d}{%s}'
'{\\sphinxtablestrut{%d}}' %
(nextcell.width, nextcell.cell_id))
(nextcell.width, colspec, nextcell.cell_id))

def visit_acks(self, node: Element) -> None:
# this is a list in the source, but should be rendered as a
Expand Down
@@ -0,0 +1,56 @@
\label{\detokenize{complex:grid-table}}

\begin{savenotes}\sphinxattablestart
\centering
\begin{tabulary}{\linewidth}[t]{TTT}
\hline
\sphinxstyletheadfamily
header1
&\sphinxstyletheadfamily
header2
&\sphinxstyletheadfamily
header3
\\
\hline
cell1\sphinxhyphen{}1
&\sphinxmultirow{2}{5}{%
\begin{varwidth}[t]{\sphinxcolwidth{1}{3}}
cell1\sphinxhyphen{}2
\par
\vskip-\baselineskip\vbox{\hbox{\strut}}\end{varwidth}%
}%
&
cell1\sphinxhyphen{}3
\\
\cline{1-1}\cline{3-3}\sphinxmultirow{2}{7}{%
\begin{varwidth}[t]{\sphinxcolwidth{1}{3}}
cell2\sphinxhyphen{}1
\par
\vskip-\baselineskip\vbox{\hbox{\strut}}\end{varwidth}%
}%
&\sphinxtablestrut{5}&
cell2\sphinxhyphen{}3
\\
\cline{2-3}\sphinxtablestrut{7}&\sphinxstartmulticolumn{2}%
\sphinxmultirow{2}{9}{%
\begin{varwidth}[t]{\sphinxcolwidth{2}{3}}
cell3\sphinxhyphen{}2
\par
\vskip-\baselineskip\vbox{\hbox{\strut}}\end{varwidth}%
}%
\sphinxstopmulticolumn
\\
\cline{1-1}
cell4\sphinxhyphen{}1
&\multicolumn{2}{l}{\sphinxtablestrut{9}}\\
\hline\sphinxstartmulticolumn{3}%
\begin{varwidth}[t]{\sphinxcolwidth{3}{3}}
cell5\sphinxhyphen{}1
\par
\vskip-\baselineskip\vbox{\hbox{\strut}}\end{varwidth}%
\sphinxstopmulticolumn
\\
\hline
\end{tabulary}
\par
\sphinxattableend\end{savenotes}
@@ -0,0 +1,45 @@
\label{\detokenize{longtable:longtable}}

\begin{savenotes}\sphinxatlongtablestart\begin{longtable}[c]{ll}
\hline
\sphinxstyletheadfamily
header1
&\sphinxstyletheadfamily
header2
\\
\hline
\endfirsthead

\multicolumn{2}{c}%
{\makebox[0pt]{\sphinxtablecontinued{\tablename\ \thetable{} \textendash{} continued from previous page}}}\\
\hline
\sphinxstyletheadfamily
header1
&\sphinxstyletheadfamily
header2
\\
\hline
\endhead

\hline
\multicolumn{2}{r}{\makebox[0pt][r]{\sphinxtablecontinued{continues on next page}}}\\
\endfoot

\endlastfoot

cell1\sphinxhyphen{}1
&
cell1\sphinxhyphen{}2
\\
\hline
cell2\sphinxhyphen{}1
&
cell2\sphinxhyphen{}2
\\
\hline
cell3\sphinxhyphen{}1
&
cell3\sphinxhyphen{}2
\\
\hline
\end{longtable}\sphinxatlongtableend\end{savenotes}
@@ -0,0 +1,30 @@
\label{\detokenize{tabular:simple-table}}

\begin{savenotes}\sphinxattablestart
\centering
\begin{tabulary}{\linewidth}[t]{TT}
\hline
\sphinxstyletheadfamily
header1
&\sphinxstyletheadfamily
header2
\\
\hline
cell1\sphinxhyphen{}1
&
cell1\sphinxhyphen{}2
\\
\hline
cell2\sphinxhyphen{}1
&
cell2\sphinxhyphen{}2
\\
\hline
cell3\sphinxhyphen{}1
&
cell3\sphinxhyphen{}2
\\
\hline
\end{tabulary}
\par
\sphinxattableend\end{savenotes}
32 changes: 32 additions & 0 deletions tests/test_build_latex.py
Expand Up @@ -1282,6 +1282,38 @@ def get_expected(name):
assert actual == expected


@pytest.mark.skipif(docutils.__version_info__ < (0, 13),
reason='docutils-0.13 or above is required')
@pytest.mark.sphinx('latex', testroot='latex-table',
confoverrides={'latex_theme_options': {'show_table_column_rules': False}})
@pytest.mark.test_params(shared_result='latex-table-nocolumn-rules')
def test_latex_table_no_column_rules(app, status, warning):
app.builder.build_all()
result = (app.outdir / 'python.tex').read_text()
tables = {}
for chap in re.split(r'\\(?:section|chapter){', result)[1:]:
sectname, content = chap.split('}', 1)
tables[sectname] = content.strip()

def get_expected(name):
return (app.srcdir / 'expects' / (name + '.tex')).read_text().strip()

# simple table
actual = tables['simple table']
expected = get_expected('simple_table_with_no_column_rules')
assert actual == expected

# longtable
actual = tables['longtable']
expected = get_expected('longtable_with_no_column_rules')
assert actual == expected

# grid table
actual = tables['grid table']
expected = get_expected('gridtable_with_no_column_rules')
assert actual == expected


@pytest.mark.sphinx('latex', testroot='latex-table',
confoverrides={'templates_path': ['_mytemplates/latex']})
def test_latex_table_custom_template_caseA(app, status, warning):
Expand Down

0 comments on commit 05a5f30

Please sign in to comment.