The ansi target, txt table styles, kdesc reference from ref/, typeface and table-width fixes (from dev fcec5e2e77ac)

Since the previous release: Machine::read_string() (the fs::path/std::string
overload pair split); kdesc -c and -u rendered from the ref/ reflection
klammerset; a fit column's tex width measured in TeX over every entry;
@typeface resolving to the family its css declares (a store directory name
now works in html) and hieroglyphs in @unicode_examples set in their
typeface; the ansi target -- plain text with the type styles a terminal can
show, identical to txt once the escape sequences are stripped -- with the
txt @table carrying :header_style, :style and :hline; @block anchoring,
@table spacing and the Image_quality state variable; and the rewritten
doc/klammertext_for_llm.md.
This commit is contained in:
ack committed 2026-09-12 18:11:48 +02:00
1 parent 760fda5470
commit 9b0d0d7897
34 files changed
+895 -394

No files matched your search

+5
View File
@@ -36,3 +36,8 @@
\newlength{\klfillb}
\newlength{\klfillc}
\newlength{\klfilld}
% Scratch register in which a 'fit' or 'fill' column's width is measured
% (\settowidth, then \maxof over each further entry) before being frozen
% into the per-column macro \klfit<X> the column spec reads.
\newlength{\klfitwidth}
+129 -41
View File
@@ -440,28 +440,33 @@ class Table(klammer_base.Klammer_base):
self.rowspan_covered.add((row_i, col_i))
self.covered = self.colspan_covered | self.rowspan_covered
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 = []
def column_width_candidates(self):
# For each column, the texts whose widths a 'fit' or 'fill' column's
# tex width is the maximum of: every cell of the column, each line
# of a multi-line cell separately, set in its own cell's font. The
# maximum is taken in TeX (tex_column_widths), not here: until
# 2026-09-12 one "longest" entry was chosen by len() of the tex
# MARKUP, so \texttt{\textasciicircum{}e'} (26 characters, 3
# visible) outranked a header "Written" and the header overflowed
# into the next column (TODO #59).
self.column_candidates = []
for col_i in range(len(self.cells[0])):
longest = ""
longest_style = "r"
candidates = []
for row_i in range(len(self.cells)):
cell = self.cells[row_i][col_i]
# 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_style = cell.font
if longest_style != "r":
longest = font.tex_fontify(longest, longest_style, 1.0)
self.column_widths.append(longest)
if cell is None or cell.colspan > 1:
continue
for line in cell.text.split("\\newline"):
line = line.strip()
if not line:
continue
if cell.font != "r":
line = font.tex_fontify(line, cell.font, 1.0)
if line not in candidates:
candidates.append(line)
self.column_candidates.append(candidates)
# ---- :hpos -------------------------------------------------------------
#
@@ -530,7 +535,7 @@ class Table(klammer_base.Klammer_base):
cell_i + max(cspan, 1) == self.row_size)))
cells.append(row_cells)
self.cells = cells
self.column_width_text()
self.column_width_candidates()
# HTML
@@ -626,6 +631,41 @@ class Table(klammer_base.Klammer_base):
# in table.sty; one per fill column, in column order.
fill_registers = ["\\klfilla", "\\klfillb", "\\klfillc", "\\klfilld"]
@staticmethod
def width_macro(col_i):
# The macro holding a column's measured width: \klfitA ... \klfitZ,
# \klfitAA, ... (a control-sequence name holds letters only).
letters = ""
i = col_i
while i >= 0:
letters = chr(ord("A") + i % 26) + letters
i = i // 26 - 1
return "\\klfit" + letters
def tex_column_widths(self):
# Set each 'fit' or 'fill' column's width macro to the width of its
# widest entry, measured in TeX: \settowidth of the first candidate,
# then calc's \maxof against \widthof of each further one, in the
# scratch register \klfitwidth (table.sty); the result is frozen
# into \klfit<X> by \edef, so the column spec -- which array's p{}
# evaluates once per CELL -- reads a number instead of re-measuring
# every candidate. Flat, one line per candidate: a nested \maxof
# would recurse as deep as the column is long.
result = ""
for i, w in enumerate(extend(self.column_width, self.row_size)):
if w not in ("fit", "f", "fill"):
continue
candidates = self.column_candidates[i]
if not candidates:
result += "\\setlength{\\klfitwidth}{0pt}\n"
else:
result += f"\\settowidth{{\\klfitwidth}}{{{candidates[0]}}}\n"
for c in candidates[1:]:
result += (f"\\setlength{{\\klfitwidth}}"
f"{{\\maxof{{\\klfitwidth}}{{\\widthof{{{c}}}}}}}\n")
result += f"\\edef{Table.width_macro(i)}{{\\the\\klfitwidth}}\n"
return result
def tex_fill_widths(self):
# Set each 'fill' column's register to min(its share of the
# remaining width, its widest entry) -- the same rule the html auto
@@ -637,11 +677,10 @@ class Table(klammer_base.Klammer_base):
if not self.fill_columns:
return ""
widths = extend(self.column_width, self.row_size)
fit = [f"\\widthof{{{self.column_widths[i]}}}"
fit = [Table.width_macro(i)
for i, w in enumerate(widths) if w in ("fit", "f")]
remaining = "\\tablewidth" + "".join(f" - {e}" for e in fit)
widest = {i: f"\\widthof{{{self.column_widths[i]}}}"
for i in self.fill_columns}
widest = {i: Table.width_macro(i) for i in self.fill_columns}
total = " + ".join(widest[i] for i in self.fill_columns)
result = ""
for k, i in enumerate(self.fill_columns):
@@ -658,7 +697,7 @@ class Table(klammer_base.Klammer_base):
def tex_hpos(self):
# One column specification per column: the width comes from
# :column_width ('fit', 'fill' via its precomputed register, a
# :column_width ('fit' and 'fill' via their precomputed macros, a
# fraction of \tablewidth, or '*' for the remaining width), the
# justification from :cell_hpos.
def par_format(s, justification):
@@ -671,7 +710,7 @@ class Table(klammer_base.Klammer_base):
fill_ordinal = 0
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]}}}")
widths.append(Table.width_macro(i))
elif w == "fill":
widths.append(Table.fill_registers[fill_ordinal])
fill_ordinal += 1
@@ -804,9 +843,9 @@ class Table(klammer_base.Klammer_base):
# marker and prints the console warning with the :column_width
# primer -- the widths are only known at LaTeX run time, and prose
# kept out of TeX avoids the log's 79-column line wrapping.
return ("\\ifdim\\tableboxwidth>\\dimexpr\\textwidth+2pt\\relax\n"
return ("\\ifdim\\tableboxwidth>\\dimexpr\\linewidth+2pt\\relax\n"
f"\\message{{^^JKT-WIDE-TABLE {name} overfull by "
"\\the\\dimexpr\\tableboxwidth-\\textwidth\\relax^^J}\n"
"\\the\\dimexpr\\tableboxwidth-\\linewidth\\relax^^J}\n"
"\\fi\n")
def tex(self):
@@ -816,15 +855,25 @@ class Table(klammer_base.Klammer_base):
# minipage below: it computes the width the minipage consumes.
measure = self.get_width() + self.tex_width_check(name)
result = f"\\renewcommand*{{\\arraystretch}}{{{self.leading}}}\n"
if self.allow_break:
result += "\\vspace*{12pt}\n"
result += "\\begin{longtable}{"
# A page-breaking table is a longtable. A boxed one is a plain
# tabular: inside its minipage a longtable could not break anyway,
# and longtable refuses to run in a multicols or two-column page,
# which is where a boxed table is wanted (until 2026-09-08 both
# paths emitted longtable, so @table could not be used in
# @multicolumn at all).
env = "longtable" if self.allow_break else "tabular"
result += f"\\begin{{{env}}}{{"
result += self.tex_column_spec()
result += "}\n"
if self.allow_break:
if self.allow_break and (self.number or self.caption):
# The caption rides in longtable's last foot so it tracks the
# table across page breaks. Without a caption there is no foot:
# an empty one is still a full row (\vspace*{8pt} plus an empty
# \small line), which put ~20pt of blank under every uncaptioned
# breaking table until 2026-09-08.
result += self.make_caption()
result += self.tex_rows()
result += "\\end{longtable}\n"
result += f"\\end{{{env}}}\n"
if not self.allow_break:
# Box the table at its measured width so the caption tracks it
@@ -845,18 +894,27 @@ class Table(klammer_base.Klammer_base):
result = measure + result
result = f"\\hypertarget{{{name}}}{{}}\\label{{Label-{name}}}\n{result}"
# The wrapper (add_caption/caption_wrapper) owns all vertical space
# around the table; longtable's own glue is zeroed.
# A boxed table's wrapper (add_caption/caption_wrapper) owns all
# vertical space around it, so longtable's own glue is zeroed. A
# page-breaking table has no wrapper and is set by longtable itself,
# so its glue is \parskip on both sides: it then sits in the text
# with the spacing of a paragraph (it was a fixed \vspace*{12pt}
# above and nothing below until 2026-09-08).
lt_glue = "\\parskip" if self.allow_break else "0pt"
result = (f"\\setlength{{\\tabcolsep}}{{{self.colsep}}}\n"
"\\setlength{\\LTpre}{0pt}\n"
"\\setlength{\\LTpost}{0pt}\n"
f"\\setlength{{\\LTpre}}{{{lt_glue}}}\n"
f"\\setlength{{\\LTpost}}{{{lt_glue}}}\n"
+ self.tex_position() +
# 2 \tabcolsep per column, minus the ones @{} removes at
# flush (lineless) edges.
f"\\setlength{{\\tablewidth}}{{\\textwidth - "
# flush (lineless) edges. \linewidth, not \textwidth: in
# a multicols column or a minipage the table must fit the
# column it is set in (the two are equal in plain text).
f"\\setlength{{\\tablewidth}}{{\\linewidth - "
f"{2 * self.row_size - self.flush_l - self.flush_r}\\tabcolsep}}\n"
# Fill widths need \tablewidth and must precede the
# measuring \savebox, whose column spec reads them.
# Column widths precede the fill widths, which read
# them; both precede the measuring \savebox, whose
# column spec reads them.
+ self.tex_column_widths()
+ self.tex_fill_widths()
+ result)
result = re.sub(r"\newline", r"\\\\", result)
@@ -907,16 +965,46 @@ class Table(klammer_base.Klammer_base):
left = space // 2
return txt_util.NBSP * left + cell + txt_util.NBSP * (space - left)
return cell + txt_util.NBSP * space
# A cell's type style, as make_cells() assigns it for html and tex:
# the column's :style code, overridden by :header_style in the
# header row. Plain text has one face, so only the styles a
# terminal can show are carried -- italic and bold, as the markers
# the ansi target resolves and the txt target strips (phases.py);
# the other codes (roman, sans, typewriter, code) style nothing.
STYLE = {"i": "italic", "b": "bold"}
def styled(cell, row_i, cell_i):
code = self.header_style if row_i == 0 and self.header else self.style[cell_i]
if cell and code in STYLE:
return (phases.txt_style_marker(STYLE[code]) + cell
+ phases.txt_style_marker(STYLE[code], False))
return cell
widths = self.txt_column_widths()
pos = [hpos(p) for p in self.cell_hpos]
result = ""
indent = txt_util.NBSP * int(self.Target_txt_indent)
colsep = " "
for row in self.rows:
# Horizontal lines are the same rule as the other targets (:hline,
# defaulting to under the header row and at the bottom when the
# table has a header): a rule of hyphens the width of the table at
# each boundary that has a line. A line over part of the columns
# is drawn full width here -- a partial rule in a plain-text table
# reads as an accident. Vertical lines have no plain-text form.
# The hyphens are HIDDEN (KTESC markers): a klammer's result is
# re-read as Klammertext and the txt target's dash transforms turn
# "--" into "-", so a bare run of hyphens came out a fraction of
# its length. (The caption rule escapes this by being drawn in a
# phase, after the transforms.)
rule = (indent + klammer_base.hide_typographic(
"-" * (sum(widths) + len(colsep) * (len(widths) - 1))) + "\n")
for row_i, row in enumerate(self.rows):
if row_i in self.s_hline.by_index:
result += rule
result += indent + colsep.join(
[pad(elt, width, align) for elt, width, align in
list(zip(row, widths, pos))])
[pad(styled(elt, row_i, cell_i), width, align)
for cell_i, (elt, width, align) in enumerate(zip(row, widths, pos))])
result += "\n"
if len(self.rows) in self.s_hline.by_index:
result += rule
result = txt_util.add_caption(
result, "Table", self.number, self.caption, self.caption_side, \
int(self.Target_txt_indent))