mirror of
https://github.com/sphinx-doc/sphinx.git
synced 2025-02-25 18:55:22 -06:00
Working on text colspan and rowspan.
This commit is contained in:
parent
ee139e155a
commit
9b30fc938d
@ -8,10 +8,12 @@
|
|||||||
:copyright: Copyright 2007-2018 by the Sphinx team, see AUTHORS.
|
:copyright: Copyright 2007-2018 by the Sphinx team, see AUTHORS.
|
||||||
:license: BSD, see LICENSE for details.
|
:license: BSD, see LICENSE for details.
|
||||||
"""
|
"""
|
||||||
|
from collections import namedtuple
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
import math
|
||||||
import textwrap
|
import textwrap
|
||||||
from itertools import groupby
|
from itertools import groupby, chain
|
||||||
|
|
||||||
from docutils import nodes, writers
|
from docutils import nodes, writers
|
||||||
from docutils.utils import column_width
|
from docutils.utils import column_width
|
||||||
@ -29,6 +31,170 @@ if False:
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Cell:
|
||||||
|
def __init__(self, text="", rowspan=1, colspan=1):
|
||||||
|
self.text = text
|
||||||
|
self.wrapped = []
|
||||||
|
self.rowspan = rowspan
|
||||||
|
self.colspan = colspan
|
||||||
|
self.col = None
|
||||||
|
self.row = None
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "<Cell {!r} {}v{}/{}>{}>".format(
|
||||||
|
self.text, self.row, self.rowspan, self.col, self.colspan
|
||||||
|
)
|
||||||
|
|
||||||
|
def __hash__(self):
|
||||||
|
return hash((self.col, self.row))
|
||||||
|
|
||||||
|
def wrap(self, width):
|
||||||
|
self.wrapped = my_wrap(self.text, width)
|
||||||
|
|
||||||
|
|
||||||
|
class Table:
|
||||||
|
def __init__(self):
|
||||||
|
self.lines = []
|
||||||
|
self.separator = 0
|
||||||
|
self.colwidth = []
|
||||||
|
self.current_line = 0
|
||||||
|
self.current_col = 0
|
||||||
|
|
||||||
|
def add_row(self):
|
||||||
|
self.current_line += 1
|
||||||
|
self.current_col = 0
|
||||||
|
|
||||||
|
def set_separator(self):
|
||||||
|
self.separator = len(self.lines)
|
||||||
|
|
||||||
|
def add_cell(self, cell):
|
||||||
|
while self[self.current_line, self.current_col]:
|
||||||
|
self.current_col += 1
|
||||||
|
self[self.current_line, self.current_col] = cell
|
||||||
|
self.current_col += cell.colspan
|
||||||
|
|
||||||
|
def __getitem__(self, pos):
|
||||||
|
line, col = pos
|
||||||
|
self._ensure_has_line(line + 1)
|
||||||
|
self._ensure_has_column(col + 1)
|
||||||
|
return self.lines[line][col]
|
||||||
|
|
||||||
|
def __setitem__(self, pos, cell):
|
||||||
|
line, col = pos
|
||||||
|
self._ensure_has_line(line + cell.rowspan)
|
||||||
|
self._ensure_has_column(col + cell.colspan)
|
||||||
|
for dline in range(cell.rowspan):
|
||||||
|
for dcol in range(cell.colspan):
|
||||||
|
self.lines[line + dline][col + dcol] = cell
|
||||||
|
cell.row = line
|
||||||
|
cell.col = col
|
||||||
|
|
||||||
|
def _ensure_has_line(self, line):
|
||||||
|
while len(self.lines) < line:
|
||||||
|
self.lines.append([])
|
||||||
|
|
||||||
|
def _ensure_has_column(self, col):
|
||||||
|
for line in self.lines:
|
||||||
|
while len(line) < col:
|
||||||
|
line.append(None)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
out = []
|
||||||
|
for line in self.lines:
|
||||||
|
out.append(repr(line))
|
||||||
|
return "\n".join(out)
|
||||||
|
|
||||||
|
def cell_width(self, cell, source):
|
||||||
|
width = 0
|
||||||
|
for i in range(self[cell.row, cell.col].colspan):
|
||||||
|
width += source[cell.col + i]
|
||||||
|
return width + (cell.colspan - 1) * 3
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cells(self):
|
||||||
|
seen = set()
|
||||||
|
for lineno, line in enumerate(self.lines):
|
||||||
|
for colno, cell in enumerate(line):
|
||||||
|
if cell and cell not in seen:
|
||||||
|
yield cell
|
||||||
|
seen.add(cell)
|
||||||
|
|
||||||
|
def rewrap(self):
|
||||||
|
self.measured_widths = self.colwidth[:]
|
||||||
|
for cell in self.cells:
|
||||||
|
cell.wrap(width=self.cell_width(cell, self.colwidth))
|
||||||
|
if not cell.wrapped:
|
||||||
|
continue
|
||||||
|
width = math.ceil(max(column_width(x) for x in cell.wrapped) / cell.colspan)
|
||||||
|
for col in range(cell.col, cell.col + cell.colspan):
|
||||||
|
self.measured_widths[col] = max(self.measured_widths[col], width)
|
||||||
|
|
||||||
|
def physical_lines_for_line(self, line):
|
||||||
|
"""From a given line, compute the number of physical lines it spans
|
||||||
|
due to text wrapping.
|
||||||
|
"""
|
||||||
|
physical_lines = 1
|
||||||
|
for cell in line:
|
||||||
|
physical_lines = max(physical_lines, len(cell.wrapped))
|
||||||
|
return physical_lines
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
out = []
|
||||||
|
self.rewrap()
|
||||||
|
|
||||||
|
def writesep(char="-", lineno=None):
|
||||||
|
# type: (unicode, Optional[int]) -> unicode
|
||||||
|
"""Called on the line *before* lineno.
|
||||||
|
Called with no lineno for the last sep.
|
||||||
|
"""
|
||||||
|
out = [] # type: List[unicode]
|
||||||
|
for colno, width in enumerate(self.measured_widths):
|
||||||
|
if (
|
||||||
|
lineno is not None
|
||||||
|
and lineno > 0
|
||||||
|
and self[lineno, colno] is self[lineno - 1, colno]
|
||||||
|
):
|
||||||
|
out.append(" " * (width + 2))
|
||||||
|
else:
|
||||||
|
out.append(char * (width + 2))
|
||||||
|
head = "+" if out[0][0] == "-" else "|"
|
||||||
|
tail = "+" if out[-1][0] == "-" else "|"
|
||||||
|
glue = [
|
||||||
|
"+" if left[0] == "-" or right[0] == "-" else "|"
|
||||||
|
for left, right in zip(out, out[1:])
|
||||||
|
]
|
||||||
|
glue.append(tail)
|
||||||
|
return head + "".join(chain(*zip(out, glue)))
|
||||||
|
|
||||||
|
for lineno, line in enumerate(self.lines):
|
||||||
|
if self.separator and lineno == self.separator:
|
||||||
|
out.append(writesep("=", lineno))
|
||||||
|
else:
|
||||||
|
out.append(writesep("-", lineno))
|
||||||
|
for physical_line in range(self.physical_lines_for_line(line)):
|
||||||
|
linestr = ["|"]
|
||||||
|
for colno, cell in enumerate(line):
|
||||||
|
if cell.col != colno:
|
||||||
|
continue
|
||||||
|
if lineno != cell.row:
|
||||||
|
physical_text = ""
|
||||||
|
elif physical_line >= len(cell.wrapped):
|
||||||
|
physical_text = ""
|
||||||
|
else:
|
||||||
|
physical_text = cell.wrapped[physical_line]
|
||||||
|
adjust_len = len(physical_text) - column_width(physical_text)
|
||||||
|
linestr.append(
|
||||||
|
" "
|
||||||
|
+ physical_text.ljust(
|
||||||
|
self.cell_width(cell, self.measured_widths) + 1 + adjust_len
|
||||||
|
)
|
||||||
|
+ "|"
|
||||||
|
)
|
||||||
|
out.append("".join(linestr))
|
||||||
|
out.append(writesep("-"))
|
||||||
|
return "\n".join(out)
|
||||||
|
|
||||||
|
|
||||||
class TextWrapper(textwrap.TextWrapper):
|
class TextWrapper(textwrap.TextWrapper):
|
||||||
"""Custom subclass that uses a different word separator regex."""
|
"""Custom subclass that uses a different word separator regex."""
|
||||||
|
|
||||||
@ -582,7 +748,7 @@ class TextTranslator(nodes.NodeVisitor):
|
|||||||
|
|
||||||
def visit_colspec(self, node):
|
def visit_colspec(self, node):
|
||||||
# type: (nodes.Node) -> None
|
# type: (nodes.Node) -> None
|
||||||
self.table[0].append(node['colwidth']) # type: ignore
|
self.table.colwidth.append(node["colwidth"]) # type: ignore
|
||||||
raise nodes.SkipNode
|
raise nodes.SkipNode
|
||||||
|
|
||||||
def visit_tgroup(self, node):
|
def visit_tgroup(self, node):
|
||||||
@ -603,7 +769,7 @@ class TextTranslator(nodes.NodeVisitor):
|
|||||||
|
|
||||||
def visit_tbody(self, node):
|
def visit_tbody(self, node):
|
||||||
# type: (nodes.Node) -> None
|
# type: (nodes.Node) -> None
|
||||||
self.table.append('sep')
|
self.table.set_separator()
|
||||||
|
|
||||||
def depart_tbody(self, node):
|
def depart_tbody(self, node):
|
||||||
# type: (nodes.Node) -> None
|
# type: (nodes.Node) -> None
|
||||||
@ -611,7 +777,8 @@ class TextTranslator(nodes.NodeVisitor):
|
|||||||
|
|
||||||
def visit_row(self, node):
|
def visit_row(self, node):
|
||||||
# type: (nodes.Node) -> None
|
# type: (nodes.Node) -> None
|
||||||
self.table.append([])
|
if self.table.lines:
|
||||||
|
self.table.add_row()
|
||||||
|
|
||||||
def depart_row(self, node):
|
def depart_row(self, node):
|
||||||
# type: (nodes.Node) -> None
|
# type: (nodes.Node) -> None
|
||||||
@ -619,79 +786,29 @@ class TextTranslator(nodes.NodeVisitor):
|
|||||||
|
|
||||||
def visit_entry(self, node):
|
def visit_entry(self, node):
|
||||||
# type: (nodes.Node) -> None
|
# type: (nodes.Node) -> None
|
||||||
if 'morerows' in node or 'morecols' in node:
|
self.entry = Cell(
|
||||||
raise NotImplementedError('Column or row spanning cells are '
|
rowspan=node.get("morerows", 0) + 1, colspan=node.get("morecols", 0) + 1
|
||||||
'not implemented.')
|
)
|
||||||
self.new_state(0)
|
self.new_state(0)
|
||||||
|
|
||||||
def depart_entry(self, node):
|
def depart_entry(self, node):
|
||||||
# type: (nodes.Node) -> None
|
# type: (nodes.Node) -> None
|
||||||
text = self.nl.join(self.nl.join(x[1]) for x in self.states.pop())
|
text = self.nl.join(self.nl.join(x[1]) for x in self.states.pop())
|
||||||
self.stateindent.pop()
|
self.stateindent.pop()
|
||||||
self.table[-1].append(text) # type: ignore
|
self.entry.text = text
|
||||||
|
self.table.add_cell(self.entry)
|
||||||
|
self.entry = None
|
||||||
|
|
||||||
def visit_table(self, node):
|
def visit_table(self, node):
|
||||||
# type: (nodes.Node) -> None
|
# type: (nodes.Node) -> None
|
||||||
if self.table:
|
if self.table:
|
||||||
raise NotImplementedError('Nested tables are not supported.')
|
raise NotImplementedError('Nested tables are not supported.')
|
||||||
self.new_state(0)
|
self.new_state(0)
|
||||||
self.table = [[]]
|
self.table = Table()
|
||||||
|
|
||||||
def depart_table(self, node):
|
def depart_table(self, node):
|
||||||
# type: (nodes.Node) -> None
|
# type: (nodes.Node) -> None
|
||||||
lines = None # type: List[unicode]
|
self.add_text(str(self.table))
|
||||||
lines = self.table[1:] # type: ignore
|
|
||||||
fmted_rows = [] # type: List[List[List[unicode]]]
|
|
||||||
colwidths = None # type: List[int]
|
|
||||||
colwidths = self.table[0] # type: ignore
|
|
||||||
realwidths = colwidths[:]
|
|
||||||
separator = 0
|
|
||||||
# don't allow paragraphs in table cells for now
|
|
||||||
for line in lines:
|
|
||||||
if line == 'sep':
|
|
||||||
separator = len(fmted_rows)
|
|
||||||
else:
|
|
||||||
cells = [] # type: List[List[unicode]]
|
|
||||||
for i, cell in enumerate(line):
|
|
||||||
par = my_wrap(cell, width=colwidths[i])
|
|
||||||
if par:
|
|
||||||
maxwidth = max(column_width(x) for x in par)
|
|
||||||
else:
|
|
||||||
maxwidth = 0
|
|
||||||
realwidths[i] = max(realwidths[i], maxwidth)
|
|
||||||
cells.append(par)
|
|
||||||
fmted_rows.append(cells)
|
|
||||||
|
|
||||||
def writesep(char='-'):
|
|
||||||
# type: (unicode) -> None
|
|
||||||
out = ['+'] # type: List[unicode]
|
|
||||||
for width in realwidths:
|
|
||||||
out.append(char * (width + 2))
|
|
||||||
out.append('+')
|
|
||||||
self.add_text(''.join(out) + self.nl)
|
|
||||||
|
|
||||||
def writerow(row):
|
|
||||||
# type: (List[List[unicode]]) -> None
|
|
||||||
lines = zip_longest(*row)
|
|
||||||
for line in lines:
|
|
||||||
out = ['|']
|
|
||||||
for i, cell in enumerate(line):
|
|
||||||
if cell:
|
|
||||||
adjust_len = len(cell) - column_width(cell)
|
|
||||||
out.append(' ' + cell.ljust(
|
|
||||||
realwidths[i] + 1 + adjust_len))
|
|
||||||
else:
|
|
||||||
out.append(' ' * (realwidths[i] + 2))
|
|
||||||
out.append('|')
|
|
||||||
self.add_text(''.join(out) + self.nl)
|
|
||||||
|
|
||||||
for i, row in enumerate(fmted_rows):
|
|
||||||
if separator and i == separator:
|
|
||||||
writesep('=')
|
|
||||||
else:
|
|
||||||
writesep('-')
|
|
||||||
writerow(row)
|
|
||||||
writesep('-')
|
|
||||||
self.table = None
|
self.table = None
|
||||||
self.end_state(wrap=False)
|
self.end_state(wrap=False)
|
||||||
|
|
||||||
|
7
tests/roots/test-build-text/table_colspan.txt
Normal file
7
tests/roots/test-build-text/table_colspan.txt
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
+-----+-----+
|
||||||
|
| XXX | XXX |
|
||||||
|
+-----+-----+
|
||||||
|
| | XXX |
|
||||||
|
+-----+ |
|
||||||
|
| XXX | |
|
||||||
|
+-----+-----+
|
@ -0,0 +1,7 @@
|
|||||||
|
+-----------+-----+
|
||||||
|
| AAA | BBB |
|
||||||
|
+-----+-----+ |
|
||||||
|
| | XXX | |
|
||||||
|
| +-----+-----+
|
||||||
|
| DDD | CCC |
|
||||||
|
+-----+-----------+
|
7
tests/roots/test-build-text/table_colspan_left.txt
Normal file
7
tests/roots/test-build-text/table_colspan_left.txt
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
+-----+-----+
|
||||||
|
| XXX | XXX |
|
||||||
|
+-----+-----+
|
||||||
|
| | XXX |
|
||||||
|
| +-----+
|
||||||
|
| XXX | |
|
||||||
|
+-----+-----+
|
7
tests/roots/test-build-text/table_rowspan.txt
Normal file
7
tests/roots/test-build-text/table_rowspan.txt
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
+-----+-----+
|
||||||
|
| XXXXXXXXX |
|
||||||
|
+-----+-----+
|
||||||
|
| | XXX |
|
||||||
|
+-----+-----+
|
||||||
|
| XXX | |
|
||||||
|
+-----+-----+
|
@ -101,6 +101,63 @@ def test_table_with_empty_cell(app, status, warning):
|
|||||||
assert lines[6] == "+-------+-------+"
|
assert lines[6] == "+-------+-------+"
|
||||||
|
|
||||||
|
|
||||||
|
@with_text_app()
|
||||||
|
def test_table_with_rowspan(app, status, warning):
|
||||||
|
app.builder.build_update()
|
||||||
|
result = (app.outdir / 'table_rowspan.txt').text(encoding='utf-8')
|
||||||
|
lines = [line.strip() for line in result.splitlines() if line.strip()]
|
||||||
|
assert lines[0] == "+-------+-------+"
|
||||||
|
assert lines[1] == "| XXXXXXXXX |"
|
||||||
|
assert lines[2] == "+-------+-------+"
|
||||||
|
assert lines[3] == "| | XXX |"
|
||||||
|
assert lines[4] == "+-------+-------+"
|
||||||
|
assert lines[5] == "| XXX | |"
|
||||||
|
assert lines[6] == "+-------+-------+"
|
||||||
|
|
||||||
|
|
||||||
|
@with_text_app()
|
||||||
|
def test_table_with_colspan(app, status, warning):
|
||||||
|
app.builder.build_update()
|
||||||
|
result = (app.outdir / 'table_colspan.txt').text(encoding='utf-8')
|
||||||
|
lines = [line.strip() for line in result.splitlines() if line.strip()]
|
||||||
|
assert lines[0] == "+-------+-------+"
|
||||||
|
assert lines[1] == "| XXX | XXX |"
|
||||||
|
assert lines[2] == "+-------+-------+"
|
||||||
|
assert lines[3] == "| | XXX |"
|
||||||
|
assert lines[4] == "+-------+ |"
|
||||||
|
assert lines[5] == "| XXX | |"
|
||||||
|
assert lines[6] == "+-------+-------+"
|
||||||
|
|
||||||
|
|
||||||
|
@with_text_app()
|
||||||
|
def test_table_with_colspan_left(app, status, warning):
|
||||||
|
app.builder.build_update()
|
||||||
|
result = (app.outdir / 'table_colspan_left.txt').text(encoding='utf-8')
|
||||||
|
lines = [line.strip() for line in result.splitlines() if line.strip()]
|
||||||
|
assert lines[0] == "+-------+-------+"
|
||||||
|
assert lines[1] == "| XXX | XXX |"
|
||||||
|
assert lines[2] == "+-------+-------+"
|
||||||
|
assert lines[3] == "| XXX | XXX |"
|
||||||
|
assert lines[4] == "| +-------+"
|
||||||
|
assert lines[5] == "| | |"
|
||||||
|
assert lines[6] == "+-------+-------+"
|
||||||
|
|
||||||
|
|
||||||
|
@with_text_app()
|
||||||
|
def test_table_with_colspan_and_rowspan(app, status, warning):
|
||||||
|
app.builder.build_update()
|
||||||
|
result = (app.outdir / 'table_colspan_and_rowspan.txt').text(encoding='utf-8')
|
||||||
|
lines = [line.strip() for line in result.splitlines() if line.strip()]
|
||||||
|
assert result
|
||||||
|
assert lines[0] == "+-------+-------+-------+"
|
||||||
|
assert lines[1] == "| AAA | BBB |"
|
||||||
|
assert lines[2] == "+-------+-------+ |"
|
||||||
|
assert lines[3] == "| DDD | XXX | |"
|
||||||
|
assert lines[4] == "| +-------+-------+"
|
||||||
|
assert lines[5] == "| | CCC |"
|
||||||
|
assert lines[6] == "+-------+-------+-------+"
|
||||||
|
|
||||||
|
|
||||||
@with_text_app()
|
@with_text_app()
|
||||||
def test_list_items_in_admonition(app, status, warning):
|
def test_list_items_in_admonition(app, status, warning):
|
||||||
app.builder.build_update()
|
app.builder.build_update()
|
||||||
|
Loading…
Reference in New Issue
Block a user