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:
2026-07-24 21:37:58 +02:00
parent 8a2699a253
commit 4262fc6136
14 changed files with 657 additions and 146 deletions

View File

@@ -25,10 +25,16 @@ index range "2-" (to the last index), or a name defined by the argument
commas; each is an index "4", a closed range "1-4", or an open range "6-"
(to the end). All indices are zero-origin.
A negative index counts from the end, as in Python: -1 is the last index,
-2 the second to last, and so on. Ranges are inclusive, so "1--2" is index
1 through the second-to-last index (the last index excluded).
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
-1 the last index
1--2 index 1 through the second-to-last index
head(1-) with table hline names: boundary 1, from column 1 on
""".strip()
@@ -38,8 +44,13 @@ class Range_error(Exception):
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*)$")
# A numeric selector is a signed integer, optionally followed by a range
# part: a separating hyphen and an optional signed end index (empty end =
# open range). The leading sign lets an index count from the end (-1 is
# the last), matching Python list indexing. The separating hyphen never
# collides with a minus sign because \d+ never consumes it.
item_rgx = re.compile(r"(?:(-?\d+)(-(-?\d+)?)?|([A-Za-z]+))(?:\(([-\d,]+)\))?$")
subset_rgx = re.compile(r"(-?\d+)(-(-?\d+)?)?$")
def hline_names(count):
@@ -49,7 +60,8 @@ def hline_names(count):
"head": [1],
"bottom": [last],
"inner": list(range(1, last)),
"all": list(range(count))}
"all": list(range(count)),
"none": []}
def vline_names(count):
@@ -57,7 +69,8 @@ def vline_names(count):
last = count - 1
return {"outer": [0, last],
"inner": list(range(1, last)),
"all": list(range(count))}
"all": list(range(count)),
"none": []}
class Indexed_range:
@@ -126,31 +139,36 @@ class Indexed_ranges:
argument = f"{self.argument} argument: " if self.argument else ""
raise Range_error(f"{argument}{message}")
def normalize(self, raw, spec, count):
"""Resolve a possibly-negative index to 0..count-1 (Python-style):
a negative index counts from the end (-1 is the last)."""
i = raw + count if raw < 0 else raw
if not 0 <= i < count:
self.error(f'In "{spec}", index {raw} is out of range '
f"(0 through {count - 1}, or -1 through -{count}).")
return i
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()
number, range_part, 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]
elif range_part is None:
indices = [self.normalize(int(number), spec, self.count)]
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}).")
first = self.normalize(int(number), spec, self.count)
last = (self.normalize(int(end), spec, self.count)
if end else self.count - 1)
if first > last:
self.error(f'In "{spec}", the index range start {first} '
f"is greater than its end {last}.")
indices = list(range(first, last + 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))
@@ -160,23 +178,24 @@ class Indexed_ranges:
entry.add_ranges(ranges)
def parse_subsets(self, spec, subsets):
# Subset indices run 0..maxval inclusive, so their count is
# maxval + 1 and a negative subset index resolves against it.
count = self.maxval + 1
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
number, range_part, end = match.groups()
if range_part is None:
start = last = self.normalize(int(number), spec, count)
else:
last = int(end) if end else self.maxval
start = self.normalize(int(number), spec, count)
last = (self.normalize(int(end), spec, count)
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