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:
1 parent
760fda5470
commit
9b0d0d7897
34 files changed
+895
-394
No files matched your search
+129
-41
@@ -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))
|
||||
|
||||
Reference in new issue
Block a user