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__()
|
||||
Reference in New Issue
Block a user