Typed arguments, calculated tables, spans, closed-world fonts, top-level fnt/ and env/
Sync with klammertext-dev through b90b0e09: - Argument types end to end: :python_cast values are applied (Python @eval receives real bools/numbers/lists), argument values are validated against their argtype patterns with the argtype's description as the error message, argtypes can declare :default (overridable per declaration), and parameterized type families are supported: rest(N) casts a rest argument to an N-dimensional list (bar-count = dimension). - Unified indexed_range syntax (selector with parenthesized subsets, composable mnemonic names) for table lines and spans. - Table klammer: caption fonts fixed in both targets, :column_width / :leading / :colsep wired, :colspan and :rowspan render (HTML attributes; \multicolumn / \multirow), calculated cell values (:calc) with prefix operators, display-precision semantics, :calc_format and :decimal period|comma. - Fonts: closed-world resolution on the Klammertext font store (infrastructure in mac/font_store; no Google Fonts links or fetch). Default fonts live in the top-level fnt/; additional fonts install into KLAMMERTEXT_FONTS directories via kdesc --font (list, samples, preview, install — classification by font metadata). CSS font family names are quoted (digit-initial families were silently lost). - Environment files moved from mac/env/ to the top-level env/; shell profiles source env/runtime.env. Dead per-host variants removed. - Container: fnt/ ships in the image; curl removed (no network use).
This commit is contained in:
197
sks/table/indexed_range.py
Normal file
197
sks/table/indexed_range.py
Normal file
@@ -0,0 +1,197 @@
|
||||
"""Parsing for the indexed_range argument syntax.
|
||||
|
||||
An indexed_range selects positions in one dimension of a grid, with an
|
||||
optional extent in the other dimension. The same syntax serves the table
|
||||
klammer's :hline and :vline arguments (index = boundary, subsets = how far
|
||||
along the line) and its :colspan and :rowspan arguments (index = row or
|
||||
column, subsets = the cells to merge). Which dimension the index selects
|
||||
is a property of the argument, not of the syntax.
|
||||
|
||||
This module replaces the former sequences.py and span.py (see debris).
|
||||
"""
|
||||
|
||||
import re
|
||||
|
||||
syntax_description = """
|
||||
An indexed_range is a selector, optionally followed by parenthesized
|
||||
subsets, written with no spaces:
|
||||
|
||||
<selector> full extent
|
||||
<selector>(<subsets>) restricted extent
|
||||
|
||||
The selector is a single index "3", a closed index range "2-5", an open
|
||||
index range "2-" (to the last index), or a name defined by the argument
|
||||
(for example "top" or "inner" for table lines). Subsets are separated by
|
||||
commas; each is an index "4", a closed range "1-4", or an open range "6-"
|
||||
(to the end). All indices are zero-origin.
|
||||
|
||||
Examples:
|
||||
3 index 3, full extent
|
||||
2-5(0-2) indices 2 through 5, each restricted to 0 through 2
|
||||
3(1-4,6-9) index 3, restricted to 1-4 and 6-9
|
||||
head(1-) with table hline names: boundary 1, from column 1 on
|
||||
""".strip()
|
||||
|
||||
|
||||
class Range_error(Exception):
|
||||
def __init__(self, message):
|
||||
super().__init__(f"{message}\n\n{syntax_description}")
|
||||
|
||||
|
||||
item_rgx = re.compile(r"(?:(\d+)(-)?(\d*)|([A-Za-z]+))(?:\(([\d,\-]+)\))?$")
|
||||
subset_rgx = re.compile(r"(\d+)(-)?(\d*)$")
|
||||
|
||||
|
||||
def hline_names(count):
|
||||
"""Boundary-name map for horizontal lines; count = row_count + 1."""
|
||||
last = count - 1
|
||||
return {"top": [0],
|
||||
"head": [1],
|
||||
"bottom": [last],
|
||||
"inner": list(range(1, last)),
|
||||
"all": list(range(count))}
|
||||
|
||||
|
||||
def vline_names(count):
|
||||
"""Boundary-name map for vertical lines; count = column_count + 1."""
|
||||
last = count - 1
|
||||
return {"outer": [0, last],
|
||||
"inner": list(range(1, last)),
|
||||
"all": list(range(count))}
|
||||
|
||||
|
||||
class Indexed_range:
|
||||
"""The selected extent for one primary-dimension index."""
|
||||
|
||||
def __init__(self, index, maxval):
|
||||
self.index = index
|
||||
self.maxval = maxval
|
||||
self.all = False # Full extent (no subsets given)
|
||||
self.ranges = [] # [[start, end], ...], inclusive
|
||||
|
||||
def add_full(self):
|
||||
self.all = True
|
||||
self.ranges = [[0, self.maxval]]
|
||||
|
||||
def add_ranges(self, ranges):
|
||||
if not self.all:
|
||||
self.ranges += ranges
|
||||
|
||||
def has(self, i):
|
||||
return any(start <= i <= end for start, end in self.ranges)
|
||||
|
||||
def items(self, invert=False):
|
||||
result = []
|
||||
for start, end in self.ranges:
|
||||
for e in range(start, end + 1):
|
||||
result.append((e, self.index) if invert else (self.index, e))
|
||||
return result
|
||||
|
||||
def __str__(self):
|
||||
subsets = ",".join([f"{s}-{e}" for s, e in self.ranges])
|
||||
return f"{self.index}({subsets})"
|
||||
|
||||
def __repr__(self):
|
||||
return self.__str__()
|
||||
|
||||
|
||||
class Indexed_ranges:
|
||||
"""A parsed indexed_range argument: Indexed_range entries by index.
|
||||
|
||||
count - number of valid primary indices (0 .. count-1)
|
||||
maxval - largest valid subset value (the cross dimension)
|
||||
specs - the argument value: a list of items (from the argtype's
|
||||
python_cast), a whitespace-separated string, or None
|
||||
names - map of selector names to index lists (hline_names, ...)
|
||||
argument - argument name for error messages (":hline", ...)
|
||||
|
||||
Items targeting the same index merge: their subsets are unioned, and a
|
||||
full-extent item absorbs any subsets.
|
||||
"""
|
||||
|
||||
def __init__(self, count, maxval, specs, names=None, argument=""):
|
||||
self.count = count
|
||||
self.maxval = maxval
|
||||
self.names = names or {}
|
||||
self.argument = argument
|
||||
self.by_index = {}
|
||||
if specs is None:
|
||||
specs = []
|
||||
elif isinstance(specs, str):
|
||||
specs = specs.split()
|
||||
for spec in specs:
|
||||
self.parse(spec)
|
||||
|
||||
def error(self, message):
|
||||
argument = f"{self.argument} argument: " if self.argument else ""
|
||||
raise Range_error(f"{argument}{message}")
|
||||
|
||||
def parse(self, spec):
|
||||
match = item_rgx.match(spec)
|
||||
if not match:
|
||||
self.error(f'"{spec}" is not a valid indexed_range.')
|
||||
number, hyphen, end, name, subsets = match.groups()
|
||||
if name is not None:
|
||||
if name not in self.names:
|
||||
known = " ".join(self.names) or "none"
|
||||
self.error(f'"{name}" is not a valid name here '
|
||||
f"(valid names: {known}).")
|
||||
indices = self.names[name]
|
||||
else:
|
||||
start = int(number)
|
||||
if not hyphen:
|
||||
indices = [start]
|
||||
else:
|
||||
last = int(end) if end else self.count - 1
|
||||
if start > last:
|
||||
self.error(f'In "{spec}", the index range start {start} '
|
||||
f"is greater than its end {last}.")
|
||||
indices = list(range(start, last + 1))
|
||||
for i in indices:
|
||||
if i >= self.count:
|
||||
self.error(f'In "{spec}", index {i} is out of range '
|
||||
f"(0 through {self.count - 1}).")
|
||||
ranges = self.parse_subsets(spec, subsets) if subsets else None
|
||||
for i in indices:
|
||||
entry = self.by_index.setdefault(i, Indexed_range(i, self.maxval))
|
||||
if ranges is None:
|
||||
entry.add_full()
|
||||
else:
|
||||
entry.add_ranges(ranges)
|
||||
|
||||
def parse_subsets(self, spec, subsets):
|
||||
ranges = []
|
||||
for part in subsets.split(","):
|
||||
match = subset_rgx.match(part)
|
||||
if not match:
|
||||
self.error(f'In "{spec}", "{part}" is not a valid subset.')
|
||||
number, hyphen, end = match.groups()
|
||||
start = int(number)
|
||||
if not hyphen:
|
||||
last = start
|
||||
else:
|
||||
last = int(end) if end else self.maxval
|
||||
if start > last:
|
||||
self.error(f'In "{spec}", the subset start {start} '
|
||||
f"is greater than its end {last}.")
|
||||
if last > self.maxval:
|
||||
self.error(f'In "{spec}", {last} is out of range '
|
||||
f"(0 through {self.maxval}).")
|
||||
ranges.append([start, last])
|
||||
return ranges
|
||||
|
||||
def __getitem__(self, index):
|
||||
return self.by_index.get(index)
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self.by_index)
|
||||
|
||||
def has(self, index, i):
|
||||
entry = self[index]
|
||||
return entry.has(i) if entry else False
|
||||
|
||||
def __str__(self):
|
||||
return " ".join([str(self.by_index[i]) for i in sorted(self.by_index)])
|
||||
|
||||
def __repr__(self):
|
||||
return self.__str__()
|
||||
@@ -1,147 +0,0 @@
|
||||
import sys
|
||||
import re
|
||||
|
||||
syntax_description = """
|
||||
|
||||
A "sequence" is an integer (the "index") followed by an optional description of one
|
||||
or more sequence subsets. A subset is defined by a series of subset
|
||||
descriptions, separated by a comma. A subset description is either an integer,
|
||||
two integers separated by a hypen to indicate a range, or an integer followed
|
||||
only by a hyphen, which will include all the following elements of the sequence to
|
||||
the end. No spaces are allowed in a sequence. All indices are zero-origin.
|
||||
|
||||
Sequence examples for an index of "3":
|
||||
3
|
||||
3(1)
|
||||
3(0-4)
|
||||
3(1-4,6-9)
|
||||
3(5-)
|
||||
|
||||
Note that some sequence subsets must include two numbers, for example, border
|
||||
lines in a table.
|
||||
|
||||
""".strip()
|
||||
|
||||
|
||||
class Sequence:
|
||||
def __init__(self, count, maxval, spec):
|
||||
def parse_range(match):
|
||||
start, hyphen, end = match.groups()
|
||||
if hyphen is None and end is None:
|
||||
end = start
|
||||
elif end is None:
|
||||
end = maxval
|
||||
return [int(start), int(end)]
|
||||
self.maxval = maxval
|
||||
self.all = True
|
||||
subset_pat = "[-\\d,]+"
|
||||
sequence_rgx = re.compile(fr"(\d+)(\({subset_pat}\))*")
|
||||
match = sequence_rgx.match(spec)
|
||||
self.spec = spec
|
||||
if match and match.group(0) == spec:
|
||||
range_rgx = re.compile(r"(\d+)(-)?(\d+)?")
|
||||
self.index = int(match.group(1))
|
||||
if (match.group(2)):
|
||||
self.subsets = match.group(2).strip("()").split(",")
|
||||
matches = [range_rgx.match(e) for e in self.subsets]
|
||||
self.ranges = [parse_range(e) if e else None for e in matches]
|
||||
self.all = False
|
||||
else:
|
||||
self.ranges = [[0, self.maxval]]
|
||||
else:
|
||||
print(f'The sequence specification "{spec}" is incorrect.\n\n{syntax_description}\n')
|
||||
sys.exit(1)
|
||||
|
||||
def __str__(self):
|
||||
#subsets = "all" if self.all else ",".join([f"{e[0]}-{e[1]}" for e in self.ranges])
|
||||
subsets = "all" if False else ",".join([f"{e[0]}-{e[1]}" for e in self.ranges])
|
||||
return f"{self.index}[{subsets}]"
|
||||
|
||||
def __repr__(self):
|
||||
return self.__str__()
|
||||
|
||||
def has(self, i):
|
||||
for start, end in self.ranges:
|
||||
if i >= start and i <= end:
|
||||
return True
|
||||
return False
|
||||
|
||||
def items(self, invert=False):
|
||||
result = []
|
||||
for start,end in self.ranges:
|
||||
for e in range(start, end+1):
|
||||
result.append((e, self.index) if invert else (self.index, e))
|
||||
return result
|
||||
|
||||
class Sequences:
|
||||
def __init__(self, count, maxval, sequence_specs):
|
||||
self.count = count
|
||||
self.maxval = maxval
|
||||
self.sequences = {}
|
||||
match_all = re.compile(r"\*(.*)").match(sequence_specs)
|
||||
match_some = re.compile(r"\[(\d+)-(\d+)\](.*)").match(sequence_specs)
|
||||
spec_list = []
|
||||
if match_all:
|
||||
subseq = match_all.group(1)
|
||||
for i in range(count):
|
||||
spec_list.append(f"{i}{subseq}")
|
||||
elif match_some:
|
||||
start = int(match_some.group(1))
|
||||
end = int(match_some.group(2))
|
||||
subseq = match_some.group(3)
|
||||
for i in range(start, end+1):
|
||||
spec_list.append(f"{i}{subseq}")
|
||||
else:
|
||||
spec_list = sequence_specs.split()
|
||||
for specs in spec_list:
|
||||
for spec in self.parse_spec(specs):
|
||||
self.sequences[spec.index] = spec
|
||||
|
||||
def __getitem__(self, index):
|
||||
return self.sequences.get(index)
|
||||
|
||||
def has(self, index, subseq_index):
|
||||
return self[index].has(subseq_index) if self[index] else None
|
||||
|
||||
def parse_spec(self, spec):
|
||||
named_spec = { "last" : [str(self.count-1)],
|
||||
"outer" : ["0", str(self.count-1)],
|
||||
"inner" : [str(e) for e in range(1, self.count - 1)],
|
||||
"all" : [str(e) for e in range(0, self.count)]
|
||||
}.get(spec)
|
||||
if named_spec is None:
|
||||
return [Sequence(self.count, self.maxval, spec)]
|
||||
else:
|
||||
return [Sequence(self.count, self.maxval, e) for e in named_spec]
|
||||
|
||||
def items(self, invert=False):
|
||||
result = []
|
||||
for seq in self.sequences:
|
||||
result += self.sequences[seq].items(invert)
|
||||
return set(result)
|
||||
|
||||
if __name__ == "__main__":
|
||||
import pprint
|
||||
for s in [
|
||||
Sequence(10, "0"),
|
||||
Sequence(10, "1"),
|
||||
Sequence(10, "2(2)"),
|
||||
Sequence(10, "3(2-)"),
|
||||
Sequence(10, "4(2-4)"),
|
||||
Sequence(10, "5(2-4)"),
|
||||
Sequence(10, "6(2-4,6)"),
|
||||
Sequence(10, "7(2,6-8)"),
|
||||
Sequence(10, "8(2,7-8,9-11,13-14)")]:
|
||||
print(s.spec, "->", s)
|
||||
|
||||
print("Sequences")
|
||||
S = Sequences(10, "0")
|
||||
for name in "top bottom head outer inner all 1(2-3)".split():
|
||||
print(name, "->", S.parse_spec(name))
|
||||
|
||||
print("Instantiate:")
|
||||
s = Sequences(10, "3(1-2,4-5) 4(8-9)")
|
||||
|
||||
#print(s.items(True))
|
||||
print(s)
|
||||
print(s.has(3,1))
|
||||
@@ -1,111 +0,0 @@
|
||||
import sys
|
||||
import re
|
||||
|
||||
syntax_description = """
|
||||
|
||||
A "span" is an integer (the "index") followed by an optional description of one
|
||||
or more sequence subsets. A subset is defined by a series of subset
|
||||
descriptions, separated by a comma. A subset description is either an integer,
|
||||
two integers separated by a hypen to indicate a range, or an integer followed
|
||||
only by a hyphen, which will include all the following elements of the span to
|
||||
the end. No spaces are allowed in a span. All indices are zero-origin.
|
||||
|
||||
Span examples for an index of "3":
|
||||
3
|
||||
3(1)
|
||||
3(0-4)
|
||||
3(1-4,6-9)
|
||||
3(5-)
|
||||
|
||||
Note that some sequence subsets must include two numbers, for example, border
|
||||
lines in a table.
|
||||
|
||||
""".strip()
|
||||
|
||||
|
||||
class Span:
|
||||
def __init__(self, count, spec):
|
||||
def parse_range(match):
|
||||
start, hyphen, end = match.groups()
|
||||
if hyphen is None and end is None:
|
||||
end = start
|
||||
elif end is None:
|
||||
end = count - 1
|
||||
return [int(start), int(end)]
|
||||
self.all = True
|
||||
subset_pat = "[-\\d,]+"
|
||||
span_rgx = re.compile(f"(\d+)(\({subset_pat}\))*")
|
||||
match = span_rgx.match(spec)
|
||||
self.spec = spec
|
||||
if match and match.group(0) == spec:
|
||||
range_rgx = re.compile("(\d+)(-)?(\d+)?")
|
||||
self.index = int(match.group(1))
|
||||
if (match.group(2)):
|
||||
self.subsets = match.group(2).strip("()").split(",")
|
||||
matches = [range_rgx.match(e) for e in self.subsets]
|
||||
self.ranges = [parse_range(e) if e else None for e in matches]
|
||||
self.all = False
|
||||
else:
|
||||
self.ranges = [[0, count-1]]
|
||||
else:
|
||||
print(f'The span specification "{spec}" is incorrect.\n\n{syntax_description}\n')
|
||||
sys.exit(1)
|
||||
|
||||
def __str__(self):
|
||||
subsets = "all" if self.all else ",".join([f"{e[0]}-{e[1]}" for e in self.ranges])
|
||||
return f"{self.index}[{subsets}]"
|
||||
|
||||
def __repr__(self):
|
||||
return self.__str__()
|
||||
|
||||
|
||||
|
||||
class Spanset:
|
||||
def __init__(self, count, span_specs):
|
||||
self.count = count
|
||||
self.spans = {}
|
||||
for specs in span_specs.split():
|
||||
for spec in self.parse_spec(specs):
|
||||
print("Spanset spec:", spec)
|
||||
self.spans[spec.index] = spec
|
||||
|
||||
|
||||
def parse_spec(self, spec):
|
||||
named_spec = {"top" : ["0"],
|
||||
"bottom" : [str(self.count-1)],
|
||||
"head" : ["1"],
|
||||
"outer" : ["0", str(self.count-1)],
|
||||
"inner" : [str(e) for e in range(1,self.count-1)],
|
||||
"all" : [str(e) for e in range(0,self.count+1)]
|
||||
}.get(spec)
|
||||
if named_spec is None:
|
||||
return [Span(self.count, spec)]
|
||||
else:
|
||||
return [Span(self.count, e) for e in named_spec]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import pprint
|
||||
for s in [
|
||||
Span(10, "0"),
|
||||
Span(10, "1"),
|
||||
Span(10, "2(2)"),
|
||||
Span(10, "3(2-)"),
|
||||
Span(10, "4(2-4)"),
|
||||
Span(10, "5(2-4)"),
|
||||
Span(10, "6(2-4,6)"),
|
||||
Span(10, "7(2,6-8)"),
|
||||
Span(10, "8(2,7-8,9-11,13-14)")]:
|
||||
print(s.spec, "->", s)
|
||||
|
||||
print("Spanset")
|
||||
S = Spanset(10, "1")
|
||||
for name in "top bottom head outer inner all 1(2-3)".split():
|
||||
print(name, "->", S.parse_spec(name))
|
||||
|
||||
print("Instantiate:")
|
||||
s = Spanset(10, "3(1-2,4-5)")
|
||||
print(s.tex_hline(3))
|
||||
|
||||
|
||||
|
||||
@@ -1,63 +1,165 @@
|
||||
#[ f: 0->1; n: 1.. (integer)
|
||||
l, c, r
|
||||
[<numeric-size>] <width> <justification>
|
||||
width is widest line in a cell f [default]
|
||||
width is fraction of table <f>t
|
||||
width is specific length <n>pt|px|in|cm [...not portable?]
|
||||
width width is remaining (evenly divided) *
|
||||
]#
|
||||
# Table argument types and klammer declaration
|
||||
|
||||
@@@argtype table_hpos |
|
||||
@@@argtype index_subsets |
|
||||
one or more subsets in parentheses, attached to an index. Each subset is a
|
||||
single index <n>, a closed range <n>-<m>, or an open range <n>- (from <n> to
|
||||
the end). Several subsets are separated by commas, with no spaces.
|
||||
Example: (1-4,6-9)
|
||||
:pattern \((?^:\d+(?^:-\d*)?)(?^:,\d+(?^:-\d*)?)*\)
|
||||
@@@
|
||||
|
||||
@@@argtype indexed_range |
|
||||
an index with optional subsets, written with no spaces. The index part is a
|
||||
single index <i>, a closed index range <i>-<j>, or an open index range <i>-
|
||||
(from <i> to the last index). It may be followed by parenthesized subsets
|
||||
(see the index_subsets type) restricting the extent in the other dimension.
|
||||
All indices are zero-origin. Examples:
|
||||
|
||||
3 index 3, full extent
|
||||
2-5 indices 2 through 5, full extent
|
||||
3(1-4,6-9) index 3, restricted to 1 through 4 and 6 through 9
|
||||
2-5(0-2) indices 2 through 5, each restricted to 0 through 2
|
||||
|
||||
:pattern \d+(?^:-\d*)?(?^:'index_subsets')?
|
||||
@@@
|
||||
|
||||
@@@argtype column_width |
|
||||
width of the table columns. Each column is one of 'fit' (widest line of the
|
||||
cells in that column), a fraction 0.0->1.0 (that fraction of the total table
|
||||
width), or '*' (use the remaining width of the table; there can only be one
|
||||
column with '*'). If there are fewer positions than columns in the table, the
|
||||
last value is repeated. Extra positions generate a warning.
|
||||
:pattern (fit^|f^|0?\.\d+^|\*^|\s+)+
|
||||
:python_cast (lambda s : s.split())
|
||||
:default fit
|
||||
@@@
|
||||
|
||||
@@@argtype cell_hpos |
|
||||
horizontal formatting in a table cell. One of 'l', 'c' or 'r' for each
|
||||
cell in a row. If there are fewer positions than cells in a row, the
|
||||
last value is repeated. Extra positions generate a warning. Default is 'l'
|
||||
# :pattern ((f^|(0?\.'uint't^|\\*))[lcr]?^|\s)+
|
||||
# :pattern ((f^|'float't^|\\*)[lcr]?^|\s)+
|
||||
:pattern ([.\w]+^|\*^|\s+)+
|
||||
#:pattern (l^|c^|r^|\s)+
|
||||
last value is repeated. Extra positions generate a warning.
|
||||
:pattern (l^|c^|r^|\s+)+
|
||||
:python_cast (lambda s : s.split())
|
||||
:default l
|
||||
@@@
|
||||
|
||||
@@@argtype table_hline |
|
||||
a table's horizontal line description; one or more of 'top',
|
||||
'head', 'inner', 'bottom', or a row number for a line at the bottom
|
||||
of that row
|
||||
#:pattern (top^|head^|inner^|bottom^|\d+^|\d+:\(\d+\-\d+\)^|\s+)*
|
||||
#:python_cast (lambda s : s.split())
|
||||
|
||||
a table's horizontal lines, as one or more whitespace-separated items.
|
||||
With N rows there are N+1 horizontal boundaries, numbered 0 to N from the
|
||||
top; boundary i lies above row i, and boundary N is the bottom. An item
|
||||
is either a boundary name or an indexed_range of boundary indices. The
|
||||
names are 'top' (boundary 0), 'head' (boundary 1, under a header row),
|
||||
'bottom' (boundary N), 'inner' (all boundaries between top and bottom),
|
||||
and 'all' (every boundary). A name or index may be followed by
|
||||
parenthesized subsets to draw only part of a line, given as zero-origin
|
||||
column ranges. Examples:
|
||||
|
||||
top bottom lines above and below the table
|
||||
head(1-) a line under the header, from column 1 to the last
|
||||
3(1-4,6-9) two partial lines at boundary 3
|
||||
all every line
|
||||
|
||||
:pattern ((?^:top^|head^|inner^|bottom^|all)(?^:'index_subsets')?^|'indexed_range'^|\s+)+
|
||||
:python_cast (lambda s : s.split())
|
||||
@@@
|
||||
|
||||
@@@argtype table_vline |
|
||||
a table's vertical line description; one or more of 'outer', 'inner,
|
||||
or a column number for a line at the right of that column
|
||||
# :pattern (outer^|inner^|\d+^|\d+^|\d+:\(\d+\-\d+\)^|\s+)*
|
||||
# :python_cast (lambda s : s.split())
|
||||
|
||||
a table's vertical lines, as one or more whitespace-separated items.
|
||||
With M columns there are M+1 vertical boundaries, numbered 0 to M from
|
||||
the left; boundary i lies to the left of column i, and boundary M is the
|
||||
right edge. An item is either a boundary name or an indexed_range of
|
||||
boundary indices. The names are 'outer' (boundaries 0 and M), 'inner'
|
||||
(all boundaries between them), and 'all' (every boundary). A name or
|
||||
index may be followed by parenthesized subsets to draw only part of a
|
||||
line, given as zero-origin row ranges. Examples:
|
||||
|
||||
outer lines at the left and right edges
|
||||
2(0-3) a line left of column 2, spanning rows 0 through 3
|
||||
all every line
|
||||
|
||||
:pattern ((?^:outer^|inner^|all)(?^:'index_subsets')?^|'indexed_range'^|\s+)+
|
||||
:python_cast (lambda s : s.split())
|
||||
@@@
|
||||
|
||||
@@@argtype table_span |
|
||||
a list of spans in a table in the form (X,Y):N (no spaces), where
|
||||
(X,Y) is the position in the table (zero origin in the top left
|
||||
corner) and N is the number of columns or rows in the span
|
||||
#:pattern (\(\d+(?:-\d+)?,\d+(?:-\d+)?\):\d+\s*)*
|
||||
#:python_cast (lambda s : s.split())
|
||||
|
||||
a list of cell spans, each an indexed_range whose index selects the row
|
||||
(for colspan) or the column (for rowspan), and whose parenthesized subset
|
||||
gives the zero-origin range of cells to merge. An index range repeats
|
||||
the same span; several subsets make several spans. Examples for colspan:
|
||||
|
||||
1(2-4) in row 1, merge columns 2 through 4
|
||||
1(0-1,3-5) two merges in row 1
|
||||
2-4(0-1) the same merge in rows 2 through 4
|
||||
|
||||
:pattern ('indexed_range'^|\s+)+
|
||||
:python_cast (lambda s : s.split())
|
||||
@@@
|
||||
|
||||
@@@argtype table_calc |
|
||||
|
||||
calculations that fill table cells with computed values, separated by
|
||||
semicolons. Each calculation has the form
|
||||
|
||||
<target> = <operator> <operand> <operand> ...
|
||||
|
||||
where the target is a single cell written <row>(<column>) with zero-origin
|
||||
indices, the operator is one of + - * /, and each operand is either a cell
|
||||
selection or a number. A cell selection is an indexed_range read as
|
||||
<rows>(<columns>); a range expands to all of its cells in row order, so
|
||||
"+ 1-2(3)" sums column 3 of rows 1 and 2. A plain number is a constant
|
||||
and always uses a period as its decimal mark. Operators fold from the
|
||||
left ("- 1(0-2)" is a minus b minus c); with a single operand, - negates
|
||||
and / gives the reciprocal. Calculations run in the order given, and each
|
||||
reads the values earlier calculations have written, as displayed.
|
||||
Example:
|
||||
|
||||
1(3) = * 1(1-2) ;
|
||||
2(3) = * 2(1-2) ;
|
||||
3(3) = + 1-2(3)
|
||||
|
||||
:pattern \s*(\d+\(\d+\)\s*=\s*[-+*/](\s+(\d+(?^:-\d*)?'index_subsets'^|'float'))+\s*(;\s*^|\s*$))+
|
||||
|
||||
@@@
|
||||
|
||||
@@@argtype decimal_mark |
|
||||
the character used as the decimal mark in numeric cell values, either
|
||||
'period' (1,234.56) or 'comma' (1.234,56). Governs both the reading of
|
||||
numbers from cells in table calculations and the formatting of
|
||||
calculated values.
|
||||
:pattern period^|comma
|
||||
:default period
|
||||
@@@
|
||||
|
||||
@@@argtype format_spec |
|
||||
a Python format specification applied to calculated cell values, for
|
||||
example ",.2f" for two decimal places with grouped thousands.
|
||||
:pattern \S+
|
||||
@@@
|
||||
|
||||
@@rowcolor.tex s : \colorrow{*s*} @@
|
||||
|
||||
@@table rows.rest
|
||||
@@table rows.rest(2)
|
||||
:id
|
||||
@caption_arguments@
|
||||
:center.bool true
|
||||
:indent.length 1em
|
||||
:header.bool true
|
||||
:allow_break.bool false
|
||||
:hline.table_hline
|
||||
:column_width.column_width
|
||||
:hline.table_hline
|
||||
:vline.table_vline
|
||||
:grid.bool false
|
||||
:cell_hpos.table_hpos c
|
||||
:cell_hpos.cell_hpos
|
||||
:header_font.font i
|
||||
:font.font_list r
|
||||
:font.font_list
|
||||
:colspan.table_span
|
||||
:rowspan.table_span
|
||||
:calc.table_calc
|
||||
:calc_format.format_spec
|
||||
:decimal.decimal_mark
|
||||
:leading.float 1.3
|
||||
:colsep 4pt
|
||||
:
|
||||
|
||||
@@ -10,13 +10,13 @@ import klammer_base
|
||||
import html_util
|
||||
from html_util import E
|
||||
import latex_util
|
||||
from sequences import Sequences
|
||||
from indexed_range import Indexed_ranges, hline_names, vline_names
|
||||
import table_cell
|
||||
import font
|
||||
|
||||
def extend(lst, count, fill=None):
|
||||
if isinstance(lst, str):
|
||||
lst = lst.strip().split("\\s+")
|
||||
lst = lst.split()
|
||||
if fill is None:
|
||||
fill = lst[-1] if lst else ""
|
||||
return lst + ([fill] * (count - len(lst)))
|
||||
@@ -44,34 +44,154 @@ class Table(klammer_base.Klammer_base):
|
||||
id = 0
|
||||
def __init__(self, K):
|
||||
super().__init__(K)
|
||||
# pprint.pprint(self.__dict__)
|
||||
if self.grid:
|
||||
self.vline = "all"
|
||||
self.hline = "all"
|
||||
self.number = self.number == "true"
|
||||
self.rows = kutil.rest_args(self.rows, 2)
|
||||
self.vline = ["all"]
|
||||
self.hline = ["all"]
|
||||
self.row_count = len(self.rows)
|
||||
if self.header:
|
||||
self.hline += f" 1 {self.row_count}"
|
||||
self.hline += ["1", str(self.row_count)]
|
||||
self.row_size = max([len(e) for e in self.rows])
|
||||
self.s_vline = Sequences(self.row_size + 1, self.row_count - 1, self.vline)
|
||||
self.s_hline = Sequences(self.row_count + 1, self.row_size - 1, self.hline)
|
||||
self.s_rowspan = Sequences(self.row_size, self.row_count, self.rowspan)
|
||||
self.s_colspan = Sequences(self.row_count, self.row_size, self.colspan)
|
||||
self.cell_hpos = extend(parse_hpos(self.K_target, self.cell_hpos.split()), self.row_size)
|
||||
# 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,
|
||||
hline_names(self.row_count + 1), ":hline")
|
||||
self.s_rowspan = Indexed_ranges(self.row_size, self.row_count - 1, self.rowspan,
|
||||
argument=":rowspan")
|
||||
self.s_colspan = Indexed_ranges(self.row_count, self.row_size - 1, self.colspan,
|
||||
argument=":colspan")
|
||||
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.make_cells(self.rows)
|
||||
|
||||
def span_count(self, span_seq, row_i, col_i):
|
||||
result = 0
|
||||
row_seq = span_seq[row_i]
|
||||
if row_seq:
|
||||
for range in row_seq.ranges:
|
||||
if range[0] == col_i:
|
||||
result = range[1] - range[0] + 1
|
||||
# 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.
|
||||
|
||||
calc_target_rgx = re.compile(r"(\d+)\((\d+)\)$")
|
||||
calc_cell_rgx = re.compile(r"\d+(-\d*)?\(")
|
||||
|
||||
def calc_error(self, calc, message):
|
||||
raise Exception(f'In the :calc calculation "{calc}": {message}')
|
||||
|
||||
def parse_number(self, text, ref, calc):
|
||||
s = text.strip()
|
||||
if self.decimal == "comma":
|
||||
s = s.translate(str.maketrans(",.", ".,"))
|
||||
s = s.replace(",", "") # Remove thousands separators
|
||||
try:
|
||||
return float(s)
|
||||
except ValueError:
|
||||
self.calc_error(
|
||||
calc, f'the cell {ref} contains "{text.strip()}", '
|
||||
"which is not a number")
|
||||
|
||||
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)
|
||||
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 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]
|
||||
result = values[0]
|
||||
for v in values[1:]: # Fold from the left
|
||||
if op == "+":
|
||||
result += v
|
||||
elif op == "-":
|
||||
result -= v
|
||||
elif op == "*":
|
||||
result *= v
|
||||
else:
|
||||
result /= v
|
||||
return result
|
||||
|
||||
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)
|
||||
|
||||
def span_count(self, spans, index, cross_i):
|
||||
# The count of cells merged by a span anchored at (index, cross_i):
|
||||
# for colspan, index is the row and cross_i the column; for rowspan,
|
||||
# index is the column and cross_i the row. Non-anchor cells get 0.
|
||||
result = 0
|
||||
entry = spans[index]
|
||||
if entry:
|
||||
for start, end in entry.ranges:
|
||||
if start == cross_i:
|
||||
result = end - start + 1
|
||||
return result
|
||||
|
||||
def compute_coverage(self):
|
||||
# Cells hidden by a span (every spanned cell except the anchor).
|
||||
self.colspan_covered = set()
|
||||
self.rowspan_covered = set()
|
||||
for row_i in self.s_colspan.by_index:
|
||||
for start, end in self.s_colspan.by_index[row_i].ranges:
|
||||
for col_i in range(start + 1, end + 1):
|
||||
self.colspan_covered.add((row_i, col_i))
|
||||
for col_i in self.s_rowspan.by_index:
|
||||
for start, end in self.s_rowspan.by_index[col_i].ranges:
|
||||
for row_i in range(start + 1, end + 1):
|
||||
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):
|
||||
@@ -83,66 +203,76 @@ class Table(klammer_base.Klammer_base):
|
||||
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
|
||||
# italic cell is measured in the font it will be set in.
|
||||
self.column_widths = []
|
||||
# print("column_width_text:", len(self.cells), len(self.cells[0]))
|
||||
for col_i in range(len(self.cells[0])):
|
||||
longest = ""
|
||||
longest_font = "r"
|
||||
for row_i in range(len(self.cells)):
|
||||
cell = self.cells[row_i][col_i]
|
||||
if cell is not None:
|
||||
cell_text = cell.text
|
||||
#lines = [e.strip() for e in cell_text.split("\\newline")]
|
||||
lines = [e.strip() for e in cell_text.split("\newline")]
|
||||
# A colspan anchor's text spans several columns and must
|
||||
# not set the width of its own column.
|
||||
if cell is not None and cell.colspan <= 1:
|
||||
lines = [e.strip() for e in cell.text.split("\\newline")]
|
||||
lines = sorted(lines, key=len)
|
||||
longest_in_line = lines[-1]
|
||||
if len(longest_in_line) > len(longest):
|
||||
longest = longest_in_line
|
||||
longest_font = cell.font
|
||||
if longest_font != "r":
|
||||
longest = font.tex_fontify(longest, longest_font, 1.0)
|
||||
self.column_widths.append(longest)
|
||||
# kutil.msg("column_widths:")
|
||||
# print(self.column_widths)
|
||||
|
||||
def make_cells(self, rows):
|
||||
self.compute_coverage()
|
||||
result = []
|
||||
cells = []
|
||||
for row_i, row in enumerate(rows):
|
||||
row_cells = []
|
||||
for cell_i, cell in enumerate(row):
|
||||
rspan = self.span_count(self.s_rowspan, row_i, cell_i)
|
||||
rspan = self.span_count(self.s_rowspan, cell_i, row_i)
|
||||
cspan = self.span_count(self.s_colspan, row_i, cell_i)
|
||||
font = self.font[cell_i]
|
||||
if row_i == 0 and self.header:
|
||||
font = self.header_font
|
||||
# A span anchor's right and bottom borders come from the
|
||||
# boundary at the END of the merged region.
|
||||
right_i = cell_i + max(cspan, 1)
|
||||
bottom_i = row_i + max(rspan, 1)
|
||||
row_cells.append(
|
||||
table_cell.Cell(
|
||||
cell,
|
||||
font, self.cell_hpos[cell_i],
|
||||
self.s_hline.has(row_i, cell_i),
|
||||
self.s_vline.has(cell_i+1, row_i),
|
||||
self.s_hline.has(row_i+1, cell_i),
|
||||
self.s_vline.has(right_i, row_i),
|
||||
self.s_hline.has(bottom_i, cell_i),
|
||||
self.s_vline.has(cell_i, row_i),
|
||||
self.s_vline.sequences.get(cell_i),
|
||||
self.s_vline.sequences.get(cell_i+1),
|
||||
rspan, cspan))
|
||||
self.s_vline.by_index.get(cell_i),
|
||||
self.s_vline.by_index.get(right_i),
|
||||
rspan, cspan,
|
||||
first_column=(cell_i == 0)))
|
||||
cells.append(row_cells)
|
||||
widths = [len(e) for e in cells]
|
||||
max_width = max(widths)
|
||||
self.cells = [extend(row, max_width, None) for row in cells] \
|
||||
if max_width != min(widths) else cells
|
||||
self.cells = cells
|
||||
self.column_width_text()
|
||||
|
||||
# HTML
|
||||
|
||||
def html(self):
|
||||
result = ""
|
||||
for row in self.cells:
|
||||
for row_i, row in enumerate(self.cells):
|
||||
row_html = ""
|
||||
for cell in row:
|
||||
for cell_i, cell in enumerate(row):
|
||||
if (row_i, cell_i) in self.covered:
|
||||
continue
|
||||
row_html += cell.html().strip() + "\n"
|
||||
result += E("tr").body(row_html).str()
|
||||
result = E("table").body(result)
|
||||
if self.number or self.caption:
|
||||
result = html_util.add_caption(
|
||||
result, "Table", self.number, self.caption, self.caption_font)
|
||||
result, "Table", self.number, self.caption, self.caption_font,
|
||||
side=self.caption_side, font_size=self.caption_font_size)
|
||||
else:
|
||||
result = result.str()
|
||||
return result
|
||||
@@ -150,6 +280,9 @@ class Table(klammer_base.Klammer_base):
|
||||
# LaTeX
|
||||
|
||||
def tex_hpos(self):
|
||||
# One column specification per column: the width comes from
|
||||
# :column_width ('fit', a fraction of \tablewidth, or '*' for the
|
||||
# remaining width), the justification from :cell_hpos.
|
||||
def par_format(s, justification):
|
||||
command = {"l" : "raggedright",
|
||||
"c" : "centering",
|
||||
@@ -157,68 +290,26 @@ class Table(klammer_base.Klammer_base):
|
||||
return f">{{\\{command}}}p{{{s}}}"
|
||||
|
||||
widths = []
|
||||
hpos_pat = re.compile("(f|(?:0?(\\.\\d+)(t))|\\*)?([lcr]?)")
|
||||
|
||||
def parse(s, width_text):
|
||||
match = hpos_pat.match(s)
|
||||
# print("MATCH:", match, match.groups())
|
||||
width, frac, table, just = match.groups()
|
||||
width = width or "f"
|
||||
just = just or "l"
|
||||
if width and width[0] == "{":
|
||||
width = f"\\widthof{{{s}}}"
|
||||
elif table == "t":
|
||||
width = f"{frac}\\tablewidth"
|
||||
elif width == "f":
|
||||
width = f"\\widthof{{{width_text}}}"
|
||||
|
||||
if width != "*":
|
||||
widths.append(width)
|
||||
|
||||
result = par_format(width, just) if width != "*" else s
|
||||
# print("PARSE:", result)
|
||||
return result
|
||||
|
||||
# print("self.column_widths:", len(self.column_widths), self.column_widths)
|
||||
# return extend([parse(e) for e in self.cell_hpos], self.row_size)
|
||||
hpos_list = []
|
||||
for i, hpos in enumerate(extend(self.cell_hpos, self.row_size)):
|
||||
# print(f" Loop {i}:", hpos)
|
||||
if i >= len(self.column_widths):
|
||||
print(f"Warning: Ignoring table column width: {hpos}")
|
||||
for i, w in enumerate(extend(self.column_width, self.row_size)):
|
||||
if w in ("fit", "f"):
|
||||
widths.append(f"\\widthof{{{self.column_widths[i]}}}")
|
||||
elif w == "*":
|
||||
widths.append(None)
|
||||
else:
|
||||
hpos_list.append(parse(hpos, self.column_widths[i]))
|
||||
|
||||
# print("hpos_list:", hpos_list)
|
||||
fill_count = sum([1 if "*" in e else 0 for e in hpos_list])
|
||||
# print("fill_count:", fill_count)
|
||||
|
||||
widths.append(f"{w}\\tablewidth")
|
||||
fill_count = widths.count(None)
|
||||
if fill_count > 0:
|
||||
margins = f"(\\tabcolsep * {2 * len(hpos_list)})"
|
||||
# print("MARGINS:", margins)
|
||||
#expr = "\\linewidth - " + " - ".join(widths) + str("
|
||||
if fill_count == len(hpos_list):
|
||||
expr = f"{1/fill_count}\\tablewidth"
|
||||
fixed = [e for e in widths if e is not None]
|
||||
if fixed:
|
||||
expr = f"(\\tablewidth - {' - '.join(fixed)}) / {fill_count}"
|
||||
else:
|
||||
#expr = f"(\\textwidth - {margins} - {' - '.join(widths)}) / {fill_count}"
|
||||
expr = f"(\\tablewidth - {' - '.join(widths)}) / {fill_count}"
|
||||
# print(expr)
|
||||
result = []
|
||||
for h in hpos_list:
|
||||
if h[0] == "*":
|
||||
just = h[1] if len(h) > 1 else "l"
|
||||
result.append(par_format(expr, just))
|
||||
else:
|
||||
result.append(h)
|
||||
else:
|
||||
result = hpos_list
|
||||
# print("tex_hpos:", result)
|
||||
|
||||
return result
|
||||
expr = f"{1 / fill_count}\\tablewidth"
|
||||
widths = [e if e is not None else expr for e in widths]
|
||||
return [par_format(w, j) for w, j in zip(widths, self.cell_hpos)]
|
||||
|
||||
def tex_column_spec(self):
|
||||
parts = [""] * (self.row_size * 2 + 1)
|
||||
for i in self.s_vline.sequences:
|
||||
for i in self.s_vline.by_index:
|
||||
parts[i * 2] = "|"
|
||||
for i, hpos in enumerate(self.tex_hpos()):
|
||||
parts[i * 2 + 1] = hpos
|
||||
@@ -226,30 +317,42 @@ class Table(klammer_base.Klammer_base):
|
||||
return "".join(parts)
|
||||
|
||||
def tex_hline(self, index):
|
||||
hline = ""
|
||||
# Contiguous cell borders coalesce into single \cline runs; a
|
||||
# full-width line becomes \hline.
|
||||
bottom = index == self.row_count
|
||||
if bottom:
|
||||
index -= 1
|
||||
count = 0
|
||||
for i, cell in enumerate(self.cells[index]):
|
||||
has_border = cell.border.bottom if bottom else cell.border.top
|
||||
if has_border:
|
||||
hline += f"\\cline{{{i+1}-{i+1}}} "
|
||||
count += 1
|
||||
#if count == self.row_size:
|
||||
# hline = "\\hline"
|
||||
flags = [(cell.border.bottom if bottom else cell.border.top)
|
||||
for cell in self.cells[index]]
|
||||
if not bottom:
|
||||
# No line through the interior of a merged (rowspan) cell.
|
||||
flags = [flag and (index, col_i) not in self.rowspan_covered
|
||||
for col_i, flag in enumerate(flags)]
|
||||
if flags and all(flags):
|
||||
return "\\hline\n"
|
||||
hline = ""
|
||||
start = None
|
||||
for i, flag in enumerate(flags + [False]):
|
||||
if flag and start is None:
|
||||
start = i
|
||||
elif not flag and start is not None:
|
||||
hline += f"\\cline{{{start + 1}-{i}}} "
|
||||
start = None
|
||||
return hline.strip() + "\n"
|
||||
|
||||
def tex_rows(self):
|
||||
result = ""
|
||||
for row_i, row in enumerate(self.cells):
|
||||
result += self.tex_hline(row_i)
|
||||
tab = ""
|
||||
parts = []
|
||||
for cell_i, cell in enumerate(row):
|
||||
result += tab + cell.tex()
|
||||
tab = " & "
|
||||
#result += " \\\\\n"
|
||||
result += " \\tabularnewline\n"
|
||||
if (row_i, cell_i) in self.colspan_covered:
|
||||
continue # Absorbed by the \multicolumn anchor
|
||||
if (row_i, cell_i) in self.rowspan_covered:
|
||||
parts.append("") # Occupied by the \multirow anchor
|
||||
else:
|
||||
parts.append(cell.tex())
|
||||
result += " & ".join(parts) + " \\tabularnewline\n"
|
||||
result += self.tex_hline(self.row_count)
|
||||
return result
|
||||
|
||||
@@ -277,9 +380,7 @@ class Table(klammer_base.Klammer_base):
|
||||
|
||||
def tex(self):
|
||||
result = self.get_width()
|
||||
result += "\\vspace*{-.75\\baselineskip}"
|
||||
# result = ""
|
||||
result += "\\renewcommand*{\\arraystretch}{1.3}\n"
|
||||
result += f"\\renewcommand*{{\\arraystretch}}{{{self.leading}}}\n"
|
||||
if self.allow_break:
|
||||
result += "\\vspace*{12pt}\n"
|
||||
result += "\\begin{longtable}{"
|
||||
@@ -289,20 +390,29 @@ class Table(klammer_base.Klammer_base):
|
||||
result += self.make_caption()
|
||||
result += self.tex_rows()
|
||||
result += "\\end{longtable}\n"
|
||||
|
||||
|
||||
if not self.allow_break:
|
||||
if self.number or self.caption:
|
||||
result = latex_util.add_caption(result, "Table", self.number, self.caption, "\\tablewidth")
|
||||
result = latex_util.add_caption(
|
||||
result, "Table", self.number, self.caption, "\\tablewidth",
|
||||
side=self.caption_side, font_symbol=self.caption_font,
|
||||
font_size=self.caption_font_size)
|
||||
else:
|
||||
result = latex_util.caption_wrapper(result, "center")
|
||||
|
||||
name = f"Reference-Table-{Table.id}"
|
||||
Table.id += 1
|
||||
result = f"\\hypertarget{{{name}}}{{}}\\label{{Label-{name}}}\n{result}"
|
||||
result = f"\\setlength{{\\tablewidth}}{{\\textwidth - {2 * self.row_count}\\tabcolsep}}\n" + result
|
||||
# result += "\\vspace*{-8pt}"
|
||||
# The wrapper (add_caption/caption_wrapper) owns all vertical space
|
||||
# around the table; longtable's own glue is zeroed.
|
||||
result = (f"\\setlength{{\\tabcolsep}}{{{self.colsep}}}\n"
|
||||
"\\setlength{\\LTpre}{0pt}\n"
|
||||
"\\setlength{\\LTpost}{0pt}\n"
|
||||
f"\\setlength{{\\tablewidth}}{{\\textwidth - {2 * self.row_size}\\tabcolsep}}\n"
|
||||
+ result)
|
||||
result = re.sub(r"\newline", r"\\\\", result)
|
||||
return result
|
||||
|
||||
def txt(self):
|
||||
return "TXT"
|
||||
return "Table in .txt format not implemented"
|
||||
|
||||
|
||||
@@ -1,19 +1,25 @@
|
||||
import re
|
||||
|
||||
import border
|
||||
import font
|
||||
import html_util
|
||||
from html_util import E
|
||||
|
||||
# A purely numeric cell value (either decimal-mark style, optional sign).
|
||||
number_rgx = re.compile(r"[-+]?[\d.,]+$")
|
||||
|
||||
class Cell:
|
||||
def __init__(self, text, font, hpos,
|
||||
top, right, bottom, left,
|
||||
left_all, right_all,
|
||||
rowspan, colspan):
|
||||
rowspan, colspan, first_column=False):
|
||||
self.text = text
|
||||
self.font = font
|
||||
self.hpos = hpos
|
||||
self.border = border.Border(top, right, bottom, left, left_all, right_all)
|
||||
self.rowspan = rowspan
|
||||
self.colspan = colspan
|
||||
self.first_column = first_column
|
||||
#print("Cell:", text, hpos)
|
||||
|
||||
def __str__(self):
|
||||
@@ -30,6 +36,10 @@ class Cell:
|
||||
def html(self):
|
||||
result = self.text
|
||||
result = E("td").body(font.html_fontify(result, self.font, 1.0))
|
||||
if self.rowspan > 1:
|
||||
result.attr("rowspan", self.rowspan)
|
||||
if self.colspan > 1:
|
||||
result.attr("colspan", self.colspan)
|
||||
for pred, cls_name in zip(self.border.has(), "Bt Br Bb Bl".split()):
|
||||
if pred:
|
||||
result.cls(cls_name)
|
||||
@@ -38,8 +48,26 @@ class Cell:
|
||||
|
||||
def tex(self, debug=False): # , left_line, right_line):
|
||||
result = self.text
|
||||
# A number must not line-break (LaTeX breaks after a minus sign
|
||||
# read as a hyphen in narrow fit-width columns).
|
||||
if number_rgx.match(result.strip()):
|
||||
result = f"\\mbox{{{result.strip()}}}"
|
||||
if self.font != "r":
|
||||
result = font.tex_fontify(result, self.font, 1.0)
|
||||
if self.rowspan > 1:
|
||||
result = f"\\multirow{{{self.rowspan}}}{{*}}{{{result}}}"
|
||||
if self.colspan > 1:
|
||||
# \multicolumn carries the merged cell's own column spec. A
|
||||
# left bar may only be given when the span starts at the
|
||||
# table's first column: elsewhere the bar to the left belongs
|
||||
# to the preceding column's preamble entry, and adding one
|
||||
# here draws a doubled line.
|
||||
pos = self.hpos
|
||||
if self.border.left and self.first_column:
|
||||
pos = "|" + pos
|
||||
if self.border.right:
|
||||
pos = pos + "|"
|
||||
return f"\\multicolumn{{{self.colspan}}}{{{pos}}}{{{result}}}"
|
||||
remove_left = self.border.left_all and not self.border.left
|
||||
remove_right = self.border.right_all and not self.border.right
|
||||
if debug:
|
||||
|
||||
Reference in New Issue
Block a user