Files
ackandClaude Fable 5.1 ed7bedf5a7 Images in plain text and the terminal; bounded build parallelism (from dev 16f82cc887af)
- @image under txt and ansi is a picture in characters, in five styles
  (Image_txt_style: halfblock the default, shade, ascii, quadrant,
  braille), sized to the terminal's width less a margin when ktext runs
  in one, every cell in its own colours under ansi; a figure's caption
  is centred under it and no txt caption is ruled off any more.
- The build compiles as many files at once as the machine has cores
  (JOBS; make -C com JOBS=1 for a small machine): the sub-makes' bare -j
  meant unlimited parallelism and exhausted 8 GB machines. The three
  install pages say "make -C com".

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-30 16:13:23 +02:00

116 lines
5.4 KiB
Python

"""Layout helpers for the txt target — the plain-text sibling of
html_util.py and latex_util.py.
What is target-specific about plain text is that it has no elements to
hang anything on: a block is set off from the prose around it by being
INDENTED, and by having laid itself out before the target sees it. Two
consequences run through this module:
* The indent is written with the NBSP MARKER, never with a real space.
Eval::eval TRIMS a klammer's result, so leading spaces never survive
to the output; and the txt target's justification pass treats a
paragraph beginning with a space or that marker as already laid out
and leaves it alone (phases.txt_justify_blocks). So the same token
both survives and declares "do not re-flow this".
* A block takes the indent out of its own measure. The page has
Target_txt_width columns; a block indented by N has that minus N, and
a block laid out to the page's width runs past the margin with
nothing to report it.
"""
import os
import re
import sys
import textwrap
sys.path.append(f'{os.environ.get("KLAMMERTEXT_HOME")}/sks/kutil')
import kutil
import klammer_base
import phases
# The non-breaking space marker, resolved to a real space at the end of
# phases.txt_justify_blocks. Stated once there, with the other txt
# markers, and referred to rather than spelled out here.
NBSP = phases.txt_nbsp_marker()
def visible_length(s):
"""How many columns s will occupy once the markers in it resolve.
len() is the wrong measure for anything laid out: a KTESC marker is
fourteen characters and one glyph, and an nbsp marker is thirteen and
one space. Measuring the unresolved text sizes a column to a width
the reader never sees -- and then pads to it, so the padding is wrong
twice. (The same fault as abbreviate() in mac/util.cpp, which counted
escape sequences as visible width, and the reason escape resolution
runs BEFORE the txt phases.)
"""
return phases.txt_visible_length(s)
def indent_text(text, indent):
"""Every line of text prefixed with `indent` columns of NBSP."""
if indent <= 0:
return text
prefix = NBSP * indent
return "\n".join(prefix + line for line in text.split("\n"))
def block_width(text_width, indent):
"""The measure available to a block indented by `indent`."""
return max(1, int(text_width) - indent)
def add_caption(element, caption_label, number, caption_text,
side="top", indent=0, centered=False):
"""Attach a caption to an already-laid-out block.
`element` is the block's text, indented by `indent` columns of NBSP.
The caption is placed above it (`side` "top", the default, and
"left") or below it ("bottom", "right"), on the same indent, so the
two read as one block and no blank line separates the caption from
what it captions.
Captions go ABOVE a table or a listing and BELOW a figure, and the
reason is not convention: a figure is taken in at a glance and the
caption then names what the reader has already seen, while a table or
a listing is READ, and a reader who meets the rows before the subject
has to read them twice. ("left"/"right" have no plain-text meaning —
a caption beside a block would need a second column — so they fall
back to the vertical side they are nearest.)
THE CAPTION IS NOT WRAPPED HERE, and must not be: when `number` is
true the text placed now is a __CAPTION__ marker, which the numbering
pass later replaces with something of a quite different length
("Table 1.2 - ..."), so any line breaks chosen now would be measured
against text that never reaches the reader. Wrapping the caption is
the numbering pass's job, once it holds the final text; it can read
the indent back off the leading NBSP run.
`caption_style` and `caption_size` are accepted by the html and tex
siblings and are absent here on purpose: plain text has one face at
one size, so there is nothing for them to select. That is stated in
@table's and @code's descriptions rather than left as a silent
difference between targets.
"""
# BOTH cases go out as a marker, the unnumbered one with an empty
# label. Everything the numbering pass does to a caption -- wrapping
# it to the block's measure, ruling it off -- it does inside the
# marker substitution, so a caption that arrived there as plain text
# got none of it: ":number false" silently lost its rule. The label
# is what says "number me", not the presence of the marker.
# `centered` rides in the marker's name as a "+center" flag, which the
# numbering pass reads off: a figure's caption is centred under it, as
# in html and tex; a table's or a listing's stays at the block's edge.
flag = "+center" if centered else ""
if number:
caption = kutil.caption_marker(caption_label + flag, caption_text, None)
elif caption_text:
caption = kutil.caption_marker(flag, caption_text, None)
else:
return element
# Marked laid-out, like the block: with no indent the caption begins
# with neither a space nor an nbsp, and the justification pass would
# re-flow it together with everything under it.
caption = phases.txt_verbatim_marker() + indent_text(
re.sub(r"\s*\n\s*", " ", caption.strip()), indent)
if side in {"top", "left"}:
return caption + "\n" + element
return element + "\n" + caption