Offer-motivated features: :hline defaults, ranged :hpos, @date :days, :bottom none
- @table: a writer's :hline/:vline replaces the default lines; new
boundary name 'none' removes all lines
- @table: ranged :hpos argument overrides :cell_hpos per cell
(\multicolumn{1} in tex; positions a colspan anchor's merged cell)
- @date/@datetime: :days offset argument (sks/date/date.py)
- @document: ":bottom none" suppresses the footer (\pagestyle{empty})
Also carries the escape-system generator/renderer fixes, per-cell-range
:format, and uppercase .TTF/.OTF font recognition from klammertext-dev.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import functools
|
||||
import collections
|
||||
import importlib
|
||||
import re
|
||||
import sys
|
||||
import traceback
|
||||
@@ -14,6 +15,15 @@ from indexed_range import Indexed_ranges, hline_names, vline_names
|
||||
import table_cell
|
||||
import font
|
||||
|
||||
# ---- :format functions ---------------------------------------------------
|
||||
# A :format function takes (value, target) and returns the formatted cell
|
||||
# text. It is named <module>.<function> in the :format list (like an eval
|
||||
# reference), so it can live in ANY module: the SKS ships none by default, and
|
||||
# a document defines its own (e.g. a euro() in the document's .py, named
|
||||
# "<module>.euro" in :format) because a specific currency style is a property
|
||||
# of that document, not of the SKS. A function should emit period-decimal
|
||||
# numbers; apply_formats() applies the :decimal comma swap.
|
||||
|
||||
def extend(lst, count, fill=None):
|
||||
if isinstance(lst, str):
|
||||
lst = lst.split()
|
||||
@@ -44,18 +54,18 @@ class Table(klammer_base.Klammer_base):
|
||||
id = 0
|
||||
def __init__(self, K):
|
||||
super().__init__(K)
|
||||
self.row_count = len(self.rows)
|
||||
if self.grid:
|
||||
self.vline = ["all"]
|
||||
self.hline = ["all"]
|
||||
self.row_count = len(self.rows)
|
||||
if self.header:
|
||||
self.hline += ["1", str(self.row_count)]
|
||||
elif not self.hline and self.header:
|
||||
# Default lines: under the header row and at the bottom. A
|
||||
# writer's own :hline replaces them (":hline none" = no lines).
|
||||
self.hline = ["1", str(self.row_count)]
|
||||
self.row_size = max([len(e) for e in self.rows])
|
||||
# Rows with fewer cells than the widest row are padded with empty
|
||||
# cells (last-value duplication is for argument lists, not content).
|
||||
self.rows = [row + [""] * (self.row_size - len(row)) for row in self.rows]
|
||||
if self.calc:
|
||||
self.calculate()
|
||||
self.s_vline = Indexed_ranges(self.row_size + 1, self.row_count - 1, self.vline,
|
||||
vline_names(self.row_size + 1), ":vline")
|
||||
self.s_hline = Indexed_ranges(self.row_count + 1, self.row_size - 1, self.hline,
|
||||
@@ -64,25 +74,58 @@ class Table(klammer_base.Klammer_base):
|
||||
argument=":rowspan")
|
||||
self.s_colspan = Indexed_ranges(self.row_count, self.row_size - 1, self.colspan,
|
||||
argument=":colspan")
|
||||
# :calc runs after the span structures exist so calculate() can warn
|
||||
# when a target lands in a cell hidden by a colspan/rowspan merge;
|
||||
# :format runs after :calc so it formats the computed values.
|
||||
if self.calc:
|
||||
self.compute_coverage()
|
||||
self.calculate()
|
||||
if self.format:
|
||||
self.apply_formats()
|
||||
self.cell_hpos = extend(parse_hpos(self.K_target, self.cell_hpos), self.row_size)
|
||||
#self.cell_hpos = self.cell_hpos.split(";")
|
||||
self.font = extend(self.font, self.row_size)
|
||||
self.hpos_map = self.hpos_overrides() if self.hpos else {}
|
||||
self.make_cells(self.rows)
|
||||
|
||||
# Calculated cell values (:calc). Calculations run in the order given;
|
||||
# each reads cell values as displayed (display-precision semantics), so
|
||||
# a printed total always equals the sum of the printed lines. Only
|
||||
# calculation targets are formatted (:calc_format); other cells keep
|
||||
# their authored text. Future operators to consider: min, max, mean,
|
||||
# and a per-calculation format override.
|
||||
# Calculated cell values (:calc). A calculation is
|
||||
# <target> = <op> <operand> ... (prefix operator: + - * /)
|
||||
# and the SHAPE of the target selects the operation:
|
||||
# * single cell r(c) -> FOLD: the operands collapse to one value.
|
||||
# * row range R(c) -> horizontal MAP: run once per row in R.
|
||||
# * column range r(C) -> vertical MAP: run once per column in C.
|
||||
# In a map the target's ranged axis iterates; an operand's aligned axis
|
||||
# iterates in lockstep and any other range in it folds. A relative
|
||||
# operand omits the iterated axis ("(col)" in a row map; bare "rows" in a
|
||||
# column map); a constant or a single fixed cell broadcasts. See
|
||||
# notes/calc_notation.md. Calculations run in order, each reading values
|
||||
# as displayed (display-precision); :format styles the results.
|
||||
# Future operators to consider: min, max, mean.
|
||||
|
||||
calc_target_rgx = re.compile(r"(\d+)\((\d+)\)$")
|
||||
calc_cell_rgx = re.compile(r"\d+(-\d*)?\(")
|
||||
# A single-axis selector: an index or an inclusive range; negatives count
|
||||
# from the end (-1 = last). A target/operand token is <rows>(<cols>),
|
||||
# <rows>, or (<cols>) -- the last two are the relative operand forms.
|
||||
selector_rgx = re.compile(r"^(-?\d+)(-(-?\d+)?)?$")
|
||||
operand_rgx = re.compile(
|
||||
r"^(?P<rows>-?\d+(?:-(?:-?\d+)?)?)?(?:\((?P<cols>[-\d,]+)\))?$")
|
||||
|
||||
def calc_error(self, calc, message):
|
||||
raise Exception(f'In the :calc calculation "{calc}": {message}')
|
||||
|
||||
def parse_number(self, text, ref, calc):
|
||||
def calc_warn(self, calc, message):
|
||||
# Non-fatal: the value is still computed and stored (a covered target
|
||||
# may be read as an operand by a later calc), it just is not rendered.
|
||||
print(f'Warning: in the :calc calculation "{calc}": {message}',
|
||||
file=sys.stderr)
|
||||
|
||||
def selector_error(self, ctx, message):
|
||||
# ctx is the caller's prefix, e.g. 'In the :calc calculation "..."' or
|
||||
# 'In :format "..."', so the same selector parser serves both.
|
||||
raise Exception(f"{ctx}: {message}")
|
||||
|
||||
def to_number(self, text):
|
||||
# A displayed cell value as a float, honoring :decimal and thousands
|
||||
# separators; None (no error raised) when it is not a number.
|
||||
s = text.strip()
|
||||
if self.decimal == "comma":
|
||||
s = s.translate(str.maketrans(",.", ".,"))
|
||||
@@ -90,44 +133,129 @@ class Table(klammer_base.Klammer_base):
|
||||
try:
|
||||
return float(s)
|
||||
except ValueError:
|
||||
self.calc_error(
|
||||
calc, f'the cell {ref} contains "{text.strip()}", '
|
||||
"which is not a number")
|
||||
return None
|
||||
|
||||
def format_number(self, value, calc):
|
||||
if self.calc_format:
|
||||
try:
|
||||
s = format(value, self.calc_format)
|
||||
except ValueError:
|
||||
self.calc_error(
|
||||
calc, f'"{self.calc_format}" is not a valid '
|
||||
"format specification")
|
||||
elif value.is_integer():
|
||||
s = str(int(value))
|
||||
else:
|
||||
s = str(value)
|
||||
def parse_number(self, text, ref, calc):
|
||||
v = self.to_number(text)
|
||||
if v is None:
|
||||
# Unescape KTESC markers so the message shows the character the
|
||||
# writer typed (e.g. "$5") rather than "KTESC0024KTESC5".
|
||||
self.calc_error(
|
||||
calc, "the cell {} contains \"{}\", which is not a number"
|
||||
.format(ref, klammer_base.unescape_ktesc(text.strip())))
|
||||
return v
|
||||
|
||||
def format_number(self, value):
|
||||
# Calc results are stored as plain numbers; :format does any styling.
|
||||
s = str(int(value)) if value.is_integer() else str(value)
|
||||
if self.decimal == "comma":
|
||||
s = s.translate(str.maketrans(",.", ".,"))
|
||||
return s
|
||||
|
||||
def operand_values(self, token, calc):
|
||||
# A token with subsets is a cell selection; a bare number is a
|
||||
# constant (always period-decimal, independent of :decimal).
|
||||
if not self.calc_cell_rgx.match(token):
|
||||
return [float(token)]
|
||||
selection = Indexed_ranges(self.row_count, self.row_size - 1,
|
||||
[token], argument=":calc")
|
||||
values = []
|
||||
for row_i in selection.by_index:
|
||||
for _, col_i in selection.by_index[row_i].items():
|
||||
values.append(self.parse_number(
|
||||
self.rows[row_i][col_i], f"{row_i}({col_i})", calc))
|
||||
return values
|
||||
def calc_norm(self, i, count, spec, ctx):
|
||||
# Resolve a possibly-negative index; -1 is the last, like Python.
|
||||
j = i + count if i < 0 else i
|
||||
if not 0 <= j < count:
|
||||
self.selector_error(
|
||||
ctx, f'in "{spec}", index {i} is out of range '
|
||||
f"(0 through {count - 1})")
|
||||
return j
|
||||
|
||||
def calc_selectors(self, spec, count, ctx):
|
||||
# "3", "0-2", "0--2", "-1", or a comma-separated list of those, to a
|
||||
# list of indices in written order (order matters for a left fold).
|
||||
result = []
|
||||
for part in spec.split(","):
|
||||
m = self.selector_rgx.match(part)
|
||||
if not m:
|
||||
self.selector_error(ctx, f'"{part}" is not a valid selector')
|
||||
start = self.calc_norm(int(m.group(1)), count, part, ctx)
|
||||
if m.group(2) is None:
|
||||
result.append(start)
|
||||
continue
|
||||
end = (self.calc_norm(int(m.group(3)), count, part, ctx)
|
||||
if m.group(3) else count - 1)
|
||||
if start > end:
|
||||
self.selector_error(
|
||||
ctx, f'in "{part}", the start {start} is after the end {end}')
|
||||
result += list(range(start, end + 1))
|
||||
return result
|
||||
|
||||
def cell_number(self, r, c, calc):
|
||||
return self.parse_number(self.rows[r][c], f"{r}({c})", calc)
|
||||
|
||||
def parse_operand(self, token, calc):
|
||||
# ('const', value) or ('cells', rows, cols) where each of rows/cols is
|
||||
# a list of indices, or None when that axis is not written (a relative
|
||||
# operand, resolved against the target's iterated axis by operand_cells).
|
||||
try:
|
||||
return ('const', float(token))
|
||||
except ValueError:
|
||||
pass
|
||||
m = self.operand_rgx.match(token)
|
||||
if not m or (m.group('rows') is None and m.group('cols') is None):
|
||||
self.calc_error(
|
||||
calc, f'"{token}" is not a number or a cell selection')
|
||||
ctx = f'In the :calc calculation "{calc}"'
|
||||
rows = (self.calc_selectors(m.group('rows'), self.row_count, ctx)
|
||||
if m.group('rows') is not None else None)
|
||||
cols = (self.calc_selectors(m.group('cols'), self.row_size, ctx)
|
||||
if m.group('cols') is not None else None)
|
||||
return ('cells', rows, cols)
|
||||
|
||||
def operand_cells(self, token, calc, mode, index, trange):
|
||||
# The operand's numbers for the current target cell. mode is 'scalar',
|
||||
# 'row' (horizontal map, rows iterate), or 'col' (vertical, cols
|
||||
# iterate); index is the current row/col; trange is the target's range.
|
||||
kind = self.parse_operand(token, calc)
|
||||
if kind[0] == 'const':
|
||||
return [kind[1]]
|
||||
_, rows, cols = kind
|
||||
if mode == 'scalar':
|
||||
if rows is None or cols is None:
|
||||
self.calc_error(
|
||||
calc, f'"{token}" is a relative operand; it needs a ranged '
|
||||
"target (a row or column range) to resolve against")
|
||||
return [self.cell_number(r, c, calc) for r in rows for c in cols]
|
||||
if mode == 'row': # rows iterate; any columns fold
|
||||
if cols is None:
|
||||
self.calc_error(
|
||||
calc, f'"{token}" selects no column; a row-map operand '
|
||||
"names a column, e.g. (0) or 0-(0)")
|
||||
if rows is None: # relative: this row
|
||||
use_rows = [index]
|
||||
elif len(rows) == 1: # a fixed row broadcasts
|
||||
use_rows = rows
|
||||
elif rows == trange: # explicit range in lockstep
|
||||
use_rows = [index]
|
||||
else:
|
||||
self.calc_error(
|
||||
calc, f'the rows of "{token}" must match the target rows')
|
||||
return [self.cell_number(r, c, calc) for r in use_rows for c in cols]
|
||||
# mode == 'col': columns iterate; any rows fold
|
||||
if rows is None:
|
||||
self.calc_error(
|
||||
calc, f'"{token}" selects no row; a column-map operand names '
|
||||
"rows, e.g. 0--2 or 0--2(0-)")
|
||||
if cols is None: # relative: this column
|
||||
use_cols = [index]
|
||||
elif len(cols) == 1: # a fixed column broadcasts
|
||||
use_cols = cols
|
||||
elif cols == trange: # explicit range in lockstep
|
||||
use_cols = [index]
|
||||
else:
|
||||
self.calc_error(
|
||||
calc, f'the columns of "{token}" must match the target columns')
|
||||
return [self.cell_number(r, c, calc) for r in rows for c in use_cols]
|
||||
|
||||
def apply_operator(self, op, values, calc):
|
||||
if len(values) == 1: # Lisp-style unary - and /
|
||||
return {"+": values[0], "*": values[0],
|
||||
"-": -values[0], "/": 1 / values[0]}[op]
|
||||
if len(values) == 1: # Lisp-style unary: - negates, / reciprocates
|
||||
v = values[0] # + and * of one operand are the operand itself
|
||||
if op == "-":
|
||||
return -v
|
||||
if op == "/": # compute 1/v only for "/", so "+ <zero cell>"
|
||||
return 1 / v # does not raise a spurious ZeroDivisionError
|
||||
return v
|
||||
result = values[0]
|
||||
for v in values[1:]: # Fold from the left
|
||||
if op == "+":
|
||||
@@ -140,31 +268,118 @@ class Table(klammer_base.Klammer_base):
|
||||
result /= v
|
||||
return result
|
||||
|
||||
def calc_fold(self, op, operands, calc, mode, index, trange):
|
||||
values = []
|
||||
for token in operands:
|
||||
values += self.operand_cells(token, calc, mode, index, trange)
|
||||
try:
|
||||
return self.apply_operator(op, values, calc)
|
||||
except ZeroDivisionError:
|
||||
self.calc_error(calc, "division by zero")
|
||||
|
||||
def calc_assign(self, r, c, value, calc, target_text):
|
||||
if (r, c) in self.covered:
|
||||
self.calc_warn(
|
||||
calc, f"the target {target_text} is a cell hidden by a colspan "
|
||||
"or rowspan merge; its computed value will not be shown")
|
||||
self.rows[r][c] = self.format_number(value)
|
||||
|
||||
def calculate(self):
|
||||
for calc in [c.strip() for c in self.calc.split(";") if c.strip()]:
|
||||
target, eq, expression = calc.partition("=")
|
||||
match = self.calc_target_rgx.match(target.strip())
|
||||
if not eq or not match:
|
||||
self.calc_error(calc, "the target must be a single cell "
|
||||
"written <row>(<column>), followed by \"=\"")
|
||||
row_i, col_i = int(match.group(1)), int(match.group(2))
|
||||
if row_i >= self.row_count or col_i >= self.row_size:
|
||||
self.calc_error(
|
||||
calc, f"the target {target.strip()} is outside the "
|
||||
f"table (rows 0-{self.row_count - 1}, "
|
||||
f"columns 0-{self.row_size - 1})")
|
||||
tokens = expression.split()
|
||||
if not tokens or tokens[0] not in "+-*/" or len(tokens) < 2:
|
||||
self.calc_error(calc, "the expression must be an operator "
|
||||
"(+ - * /) followed by at least one operand")
|
||||
values = []
|
||||
for token in tokens[1:]:
|
||||
values += self.operand_values(token, calc)
|
||||
try:
|
||||
result = self.apply_operator(tokens[0], values, calc)
|
||||
except ZeroDivisionError:
|
||||
self.calc_error(calc, "division by zero")
|
||||
self.rows[row_i][col_i] = self.format_number(result, calc)
|
||||
self.run_calc(calc)
|
||||
|
||||
def run_calc(self, calc):
|
||||
target, eq, expression = calc.partition("=")
|
||||
target = target.strip()
|
||||
m = self.operand_rgx.match(target)
|
||||
if not eq or not m or m.group('rows') is None or m.group('cols') is None:
|
||||
self.calc_error(
|
||||
calc, "the target must be a cell r(c) or a ranged cell such as "
|
||||
"0-(2) or -1(0-), followed by \"=\"")
|
||||
ctx = f'In the :calc calculation "{calc}"'
|
||||
trows = self.calc_selectors(m.group('rows'), self.row_count, ctx)
|
||||
tcols = self.calc_selectors(m.group('cols'), self.row_size, ctx)
|
||||
tokens = expression.split()
|
||||
if not tokens or tokens[0] not in ("+", "-", "*", "/") or len(tokens) < 2:
|
||||
self.calc_error(
|
||||
calc, "the expression must be an operator (+ - * /) followed by "
|
||||
"at least one operand")
|
||||
op, operands = tokens[0], tokens[1:]
|
||||
row_range, col_range = len(trows) > 1, len(tcols) > 1
|
||||
if row_range and col_range:
|
||||
self.calc_error(
|
||||
calc, "the target may range over rows or columns, but not both")
|
||||
if not row_range and not col_range: # single cell: a fold
|
||||
v = self.calc_fold(op, operands, calc, 'scalar', None, None)
|
||||
self.calc_assign(trows[0], tcols[0], v, calc, target)
|
||||
elif row_range: # horizontal map
|
||||
for r in trows:
|
||||
v = self.calc_fold(op, operands, calc, 'row', r, trows)
|
||||
self.calc_assign(r, tcols[0], v, calc, target)
|
||||
else: # vertical map
|
||||
for c in tcols:
|
||||
v = self.calc_fold(op, operands, calc, 'col', c, tcols)
|
||||
self.calc_assign(trows[0], c, v, calc, target)
|
||||
|
||||
# ---- :format ----------------------------------------------------------
|
||||
#
|
||||
# ";"-separated <cells> <function> pairs (same list style as :calc).
|
||||
# <cells> is an indexed_range; <function> is a "<module>.<function>"
|
||||
# reference (like an @eval reference) to a Python function taking
|
||||
# (value, target) and returning the formatted cell text. Each selected
|
||||
# cell's value is parsed as a number (honoring :decimal); if numeric the
|
||||
# function is called and its result -- with the :decimal comma swap
|
||||
# applied -- replaces the cell (e.g. a writer's myformats.euro function
|
||||
# turns 1234.56 into "1,234.56 €"). A
|
||||
# non-numeric cell is left as-is with a warning. Runs AFTER :calc. The
|
||||
# result is inserted verbatim (this pass is after the cell-processing pass),
|
||||
# so a function may emit target markup directly.
|
||||
|
||||
def format_warn(self, message):
|
||||
print(f"Warning: in :format: {message}", file=sys.stderr)
|
||||
|
||||
def format_function(self, spec, ctx):
|
||||
# Resolve "<module>.<function>" to a callable, like an @eval reference.
|
||||
if "." not in spec:
|
||||
self.selector_error(
|
||||
ctx, f'the format function "{spec}" must be written '
|
||||
"<module>.<function>, e.g. table.euro")
|
||||
mod_name, func_name = spec.rsplit(".", 1)
|
||||
try:
|
||||
return getattr(importlib.import_module(mod_name), func_name)
|
||||
except (ImportError, AttributeError):
|
||||
self.selector_error(
|
||||
ctx, f'the format function "{spec}" was not found')
|
||||
|
||||
def apply_formats(self):
|
||||
for stmt in [s.strip() for s in self.format.split(";") if s.strip()]:
|
||||
ctx = f'In :format "{stmt}"'
|
||||
parts = stmt.split()
|
||||
if len(parts) != 2:
|
||||
self.selector_error(
|
||||
ctx, "each entry is <cells> <function>, e.g. 0-(5) myformats.euro")
|
||||
rangespec, spec = parts
|
||||
func = self.format_function(spec, ctx)
|
||||
m = self.operand_rgx.match(rangespec)
|
||||
if not m or m.group('rows') is None or m.group('cols') is None:
|
||||
self.selector_error(
|
||||
ctx, f'"{rangespec}" is not a cell range like 0-(5) '
|
||||
"or 1--2(0-3)")
|
||||
rows = self.calc_selectors(m.group('rows'), self.row_count, ctx)
|
||||
cols = self.calc_selectors(m.group('cols'), self.row_size, ctx)
|
||||
for r in rows:
|
||||
for c in cols:
|
||||
v = self.to_number(self.rows[r][c])
|
||||
if v is None:
|
||||
self.format_warn(
|
||||
f'cell {r}({c}) contains "'
|
||||
f'{klammer_base.unescape_ktesc(self.rows[r][c].strip())}'
|
||||
'", which is not a number; left unformatted')
|
||||
continue
|
||||
result = func(v, self.K_target)
|
||||
if self.decimal == "comma":
|
||||
result = result.translate(str.maketrans(",.", ".,"))
|
||||
self.rows[r][c] = result
|
||||
|
||||
def span_count(self, spans, index, cross_i):
|
||||
# The count of cells merged by a span anchored at (index, cross_i):
|
||||
@@ -192,16 +407,6 @@ class Table(klammer_base.Klammer_base):
|
||||
self.rowspan_covered.add((row_i, col_i))
|
||||
self.covered = self.colspan_covered | self.rowspan_covered
|
||||
|
||||
def remove_redundant_borders(self):
|
||||
remove_right = []
|
||||
for row_i in range(self.row_count):
|
||||
for cell_i in range(self.row_size):
|
||||
a = self.cells[row_i][cell_i]
|
||||
b = self.cells[row_i][cell_i+1]
|
||||
if a.border.right and b.border.left:
|
||||
a.border.right = False
|
||||
a.border.right_all = False
|
||||
|
||||
def column_width_text(self):
|
||||
# For each column, the text used to measure a 'fit' width in the
|
||||
# tex target. The longest cell's font is applied, so a bold or
|
||||
@@ -225,6 +430,36 @@ class Table(klammer_base.Klammer_base):
|
||||
longest = font.tex_fontify(longest, longest_font, 1.0)
|
||||
self.column_widths.append(longest)
|
||||
|
||||
# ---- :hpos -------------------------------------------------------------
|
||||
#
|
||||
# ";"-separated <cells> <position> pairs (the same list style as :calc
|
||||
# and :format). <cells> is an indexed_range; <position> is l, c, or r
|
||||
# and overrides the column's :cell_hpos for the selected cells. A
|
||||
# colspan anchor's override positions the whole merged cell; in the tex
|
||||
# target an ordinary overridden cell is wrapped in \multicolumn{1}.
|
||||
|
||||
def hpos_overrides(self):
|
||||
result = {}
|
||||
for stmt in [s.strip() for s in self.hpos.split(";") if s.strip()]:
|
||||
ctx = f'In :hpos "{stmt}"'
|
||||
parts = stmt.split()
|
||||
if len(parts) != 2 or parts[1] not in ("l", "c", "r"):
|
||||
self.selector_error(
|
||||
ctx, "each entry is <cells> <position>, the position one "
|
||||
"of l, c, or r -- e.g. -3--1(3) r")
|
||||
rangespec, pos = parts
|
||||
m = self.operand_rgx.match(rangespec)
|
||||
if not m or m.group('rows') is None or m.group('cols') is None:
|
||||
self.selector_error(
|
||||
ctx, f'"{rangespec}" is not a cell range like 0-(5) '
|
||||
"or 1--2(0-3)")
|
||||
rows = self.calc_selectors(m.group('rows'), self.row_count, ctx)
|
||||
cols = self.calc_selectors(m.group('cols'), self.row_size, ctx)
|
||||
for r in rows:
|
||||
for c in cols:
|
||||
result[(r, c)] = pos
|
||||
return result
|
||||
|
||||
def make_cells(self, rows):
|
||||
self.compute_coverage()
|
||||
result = []
|
||||
@@ -241,10 +476,11 @@ class Table(klammer_base.Klammer_base):
|
||||
# boundary at the END of the merged region.
|
||||
right_i = cell_i + max(cspan, 1)
|
||||
bottom_i = row_i + max(rspan, 1)
|
||||
hpos = self.hpos_map.get((row_i, cell_i), self.cell_hpos[cell_i])
|
||||
row_cells.append(
|
||||
table_cell.Cell(
|
||||
cell,
|
||||
font, self.cell_hpos[cell_i],
|
||||
font, hpos,
|
||||
self.s_hline.has(row_i, cell_i),
|
||||
self.s_vline.has(right_i, row_i),
|
||||
self.s_hline.has(bottom_i, cell_i),
|
||||
@@ -252,7 +488,8 @@ class Table(klammer_base.Klammer_base):
|
||||
self.s_vline.by_index.get(cell_i),
|
||||
self.s_vline.by_index.get(right_i),
|
||||
rspan, cspan,
|
||||
first_column=(cell_i == 0)))
|
||||
first_column=(cell_i == 0),
|
||||
hpos_forced=(row_i, cell_i) in self.hpos_map))
|
||||
cells.append(row_cells)
|
||||
self.cells = cells
|
||||
self.column_width_text()
|
||||
|
||||
Reference in New Issue
Block a user