Option sets: a .o target for shared parameters

A named group of optional parameters, declared once and used by several
klammers, so a writer learns one vocabulary instead of a spelling per
klammer.  The "o" target is a pseudo-target beside "k": "k" declares a
klammer's interface and documents it, "o" declares an option interface and
documents it, and neither produces output for any target.

    @@caption_args.o :caption :number.bool true :caption_side.side
    : Arguments that define a caption for a block element @@

    @@code.k :filename @hpos_args :hpos left @ @caption_args :caption_side top @
    | text.literal : A source file displayed verbatim @@

A set is used only in the parameter list of a ".k" declaration -- the one
place a klammer's interface is declared once for all of its targets -- and
is resolved as that list is read.  Names and types come from the set; a
default may be overridden where it is used.  A klammer application in a
parameter list is now a definition-time error.

The SKS gains the sets caption_args and hpos_args (:hpos and :offset), and
@table, @image, @image_grid, @reference and @show gain .k declarations.  A
distance is no longer written as a position: :hpos 4em is rejected, and the
same layout is :hpos left :offset 4em.  Code listings are numbered by
default, like tables and figures.

New engine sources mac/option_set{,_registry}.{h,cpp}; tst/ ships two more
suites, option_set_test.sh and signature_test.sh (twelve in all).

 (from dev 34e536cb0329)
This commit is contained in:
2026-08-06 13:11:37 +02:00
parent 4306dcd490
commit 6e7596ab2e
37 changed files with 2198 additions and 267 deletions

View File

@@ -1,3 +1,26 @@
#[
A paragraph is a document-structural object, like a section or a table of
contents, and making paragraphs is a service @document provides: inside a
@document, blank-line-separated text becomes paragraphs (the LaTeX
convention) and @par need not be written. @par is the explicit form, for
a text FRAGMENT rendered without a @document, where nothing infers them.
Written inside a @document it is harmless: html sees a <p>, which the
paragraph pass recognizes as a block and leaves alone, and \par in
vertical mode is a no-op.
]#
@@par.k s : A paragraph of text @@
@@par.html :: <p>*s*</p> @@
@@par.tex ::
\par
*s*
\par
@@
# In plain text a paragraph is delimited by blank lines, which
# phases.justify_blocks then fills; #/2 inserts them without depending on
# the definition body's own whitespace surviving extraction.
@@par.txt :: #/2*s*#/2 @@
@@sp.k : Non-breaking space character @@
@@sp.html :: &^#160; @@
@@sp.tex :: ~ @@

View File

@@ -27,7 +27,7 @@ class Indent(klammer_base.Klammer_base):
result = tab + latex_util.minipage(
"\\raggedright " + self.s, f"\\textwidth - {self.w}ex", center=False, vmargin="4pt")
#print(result)
return result
return latex_util.block(result)
def txt(self):
indent = " " * self.w
@@ -54,13 +54,15 @@ class Note(klammer_base.Klammer_base):
#width = r'\\textwidth - 16pt - {}\\leftmargin'.format(self.level)
width = r'\\linewidth - \\leftmargin + 2pt'
# The \par on each side comes from latex_util.block() below: an
# \fcolorbox is box material and must sit in vertical mode.
result = '''
\\par\\begingroup
\\begingroup
COLOR\\setlength{\\fboxsep}{8pt}
\\fcolorbox{bordercolor}{localcolor}{
\\parbox{WIDTH}{\\raggedright\\setlength{\\parskip}{8pt}
\\textbf{LABEL:} TEXT
}}\\endgroup\\par
}}\\endgroup
'''
result = re.sub('LABEL', self.label, result)
result = re.sub('TEXT', re.sub(r'\\', r'\\\\', self.s), result)
@@ -68,7 +70,7 @@ COLOR\\setlength{\\fboxsep}{8pt}
result = re.sub('COLOR', r'\\definecolor{{bordercolor}}{{rgb}}{{{}}}\n'.format(self.bordercolor), result)
result = re.sub('WIDTH', width, result)
print(result)
return result
return latex_util.block(result)
class Block(klammer_base.Klammer_base):
@@ -80,6 +82,9 @@ class Block(klammer_base.Klammer_base):
return ""
def tex(self):
# NOT latex_util.block(): a textblock is absolutely positioned and
# does not participate in the normal flow, so breaking the paragraph
# around it would move the surrounding text.
to_x, to_y = [float(e) for e in self.to.split()]
pt_x, pt_y = [float(e) for e in self.point.split()]
result = f"""

View File

@@ -1,128 +1,20 @@
@@code.k :filename :pattern :caption :number.bool | text.literal :
@@code.k :filename :pattern
@hpos_args :hpos left @
@caption_args :caption_side top @
| text.literal :
A source file displayed verbatim
@@
@@code :: @eval code_format.Code(K) @ @@
@@code :: @eval code_block.Code(K) @ @@
@@c.k code_text :
A word or phrase displayed verbatim in a line
@@
@@c :: @eval code_format.Code_fragment(K) eval@
@@c :: @eval code_block.Code_fragment(K) eval@
@@
# :cwd makes the filename resolve against the DOCUMENT's directory, not
# the directory ktext happens to run in.
@@source_file filename : @eval :cwd *K_input_dir* code_format.Source(K) @ @@
#[
@@code.k text :file :number.bool true :caption : Code listing with formatted comments @@
@@code :
@eval code_format.Code(K) eval@
@@
@@lst spec.figure_id :
@reference *spec* | Listing @
@@
c <text>
code <text> :caption :filename :pattern
# --------------------------------------------------------------------------------
@@codebox.k s :color 1,1,1 :size normalsize :escapechar ^^
:space_break_only.bool false :linenumber.bool false :scale 1.0
:indent :standalone :vcenter :
Verbatim text for source code preserving whitepace, surrounded by a box
that extends to the margins @@
@@codebox.html ::
@code :text *s* @
@@
@@codebox.tex ::
\definecolor{codeboxbgcolor}{rgb}{*color*}
\setlength{\codeboxlinelength}{\linewidth - 6pt}
\vspace*{4pt}
\begin{lstlisting}%
[frame=single,
framerule=1pt,
basicstyle=\*size*\ttfamily,
lineskip=0pt,
linewidth=*scale*\codeboxlinelength,
columns=fullflexible,
keepspaces=true,
framesep=6pt,
xleftmargin=6pt,
escapechar=*escapechar*,
breaklines=true,
prebreak=\hbox{\large$\mapsto$},
%postbreak={\textbf{\hbox{$\rightarrow$}}},
rulecolor=\color{codeboxcolor},
backgroundcolor=\color{codeboxbgcolor},
breakatwhitespace=*space_break_only*,
numbers=none, # #- @if *linenumber* | left | none @ #- ,
numbersep=12pt,
numberstyle=\small\color{Darkred}]
*s*
\end{lstlisting}
@@
@@codebox.txt ::
@code :text *s* @
@@
@@pathname.k s :small.bool false : Pathname @@
@@pathname :: @eval code_format.Pathname(K) eval@ @@
@@annotate.k text :caption :
Comments put in boxes to the right of the code
@@
# @@annotate : @eval code_format.Annotate(K) eval@ @@
# @@annotate : @codebox *text* @ @@
@@annotate :: @code :text *text @ @@
@@listing s : @code :text *s* @ @@
# --------------------------------------------------------------------------------
@@sv.k s : Italic font for variable in @t syntax @ argument @@
@@sv.tex :: ^^textrm"^^textit"*s*$$ @@
@@sv.html :: @ri *s* @ @@
@@svs.k s : Sans-serif font for variable in @t syntax @ argument @@
@@svs.tex :: "^^small^^textsf"^^textit"*s*$$$ @@
@@svs.html :: @s @i *s* @ @ @@
@@svsub.k base | sub : Italic font for subscripted variable in @t syntax @ argument @@
@@svsub.tex :: ^^textrm"^^textit"*base*$$^^textsubscript"*sub*$ @@
@@svsub.html :: <span class="ritalic">*base*</span><sub>*sub*</sub> @@
@@syntax.k s :fontsize normalsize :indent.bool true :
Verbatim text that includes italic font for syntax descriptions @@
@@syntax.html ::
@code :text *s* @ # :indent *indent* @
@@
@@syntax.tex ::
\vspace*{8pt}\begin{LVerbatim}[xleftmargin=0pt, # @if true | 0pt | -24pt @ ,
baselinestretch=1.05, fontsize=\*fontsize*, frame=single, framesep=8pt,
commandchars=\\̈\$, fontfamily = @verbatimfont@, framesep=12pt]
*s*
\end{LVerbatim}
@@
@@svspace.tex length :
^^vspace*"*length*$
@@
@@svspace.html length :
@@
]#
@@source_file filename : @eval :cwd *K_input_dir* code_block.Source(K) @ @@

361
sks/code/code_block.py Normal file
View File

@@ -0,0 +1,361 @@
if __name__ == "__main__":
import sys
sys.path.append("../kutil")
sys.path.append("../target")
import sys
import re
import klammer_base
import kutil
import html_util
from html_util import E
import latex_util as L
import pprint
import phases
def escape_newlines(s):
# A newline becomes the marker the html paragraph pass turns back into a
# line break, so a verbatim source file keeps its lines. Used by Source.
return re.sub("\n", " ___NL___ ", s)
def undash(s):
# Verbatim text must show the hyphens the writer typed: the target's
# "--"/"---" transforms have already run, so put them back. Used by
# Code_fragment.
result = re.sub("__MDASH__", "---", s)
return re.sub("__NDASH__", "--", result)
def is_comment(s):
return s.strip().startswith("//")
def split_blocks(s):
kutil.msg()
blocks = []
in_code = True
block = ""
for line in s.rstrip().split("\n"):
if is_comment(line):
if in_code:
blocks.append(block)
block = line + "\n"
in_code = False
else:
block += line + "\n"
else:
if not in_code:
blocks.append(block)
block = line + "\n"
in_code = True
else:
block += line + "\n"
if block:
blocks.append(block)
return blocks
def get_lines(comment, code, n):
kutil.msg()
lines = code.split("\n")
if len(lines) < n:
raise Exception(
f"Error in @code block comment:\n{comment}\n{code}\nNeed {n} lines, but there are only {len(lines)}")
if len(lines) == n:
return (code, "")
else:
return ("\n".join(lines[:n]), "\n".join(lines[n:]))
def parse_blocks(blocks):
kutil.msg()
box_comment_rgx = re.compile("\\s*//(\\d+)\\s+.*", re.S)
i = 0
block_pairs = []
while i < len(blocks):
match = box_comment_rgx.match(blocks[i])
if match:
boxed_code, rest_code = get_lines(blocks[i], blocks[i+1], int(match.group(1)))
block_pairs.append([boxed_code, blocks[i]])
block_pairs.append([rest_code, ""])
i += 2
else:
block_pairs.append([blocks[i], ""])
i += 1
return block_pairs
# Characters that are special to LATEX but not to Klammertext. # and ^ are
# deliberately ABSENT: they are Klammertext specials and are handled by
# quote_specials() below, not here -- see tex_line().
latex_escapes = {
"\\": "\\textbackslash{}", "{": "\\{", "}": "\\}", "%": "\\%",
"$": "\\$", "&": "\\&", "_": "\\_", "~": "\\textasciitilde{}",
"<": "\\textless{}", ">": "\\textgreater{}",
}
latex_escape_rgx = re.compile("[" + re.escape("".join(latex_escapes)) + "]")
def escape_latex(s):
r"""Escape LaTeX special characters in code text.
ONE pass, not a sequence of str.replace() calls: a sequence corrupts
the replacements it has already made. Replacing \ first yields
\textbackslash{}, whose braces the later { and } replacements then
escape in turn -- so a C++ "\n" typesets as \{}n. A single regex pass
over the original string cannot revisit its own output.
"""
return latex_escape_rgx.sub(lambda m: latex_escapes[m.group()], s)
def quote_specials(s):
r"""Quote the Klammertext special characters in code text.
An @eval result is RE-READ as Klammertext, so a special character that
reaches the result raw is interpreted again: a # in "#include" starts a
text removal and silently eats the rest of the line. Quoting is the
fix, and it must be quoting rather than LaTeX escaping -- \# would put
the # back and be eaten in turn.
The quoted forms resolve at final processing: the tex target declares
^# -> \# and ^^ -> \textasciicircum{} in its :escape list, and any
other quoted special decodes back to its own character. So the target,
not this code, decides what a # becomes in LaTeX.
^ is quoted FIRST, since the other quotings introduce ^ characters.
"""
for ch in "^@#|":
s = s.replace(ch, "^" + ch)
return s
def tex_line(line):
r"""One code line, ready to be a \klline argument.
Three steps in this order: LaTeX-escape the characters only LaTeX cares
about; quote the ones Klammertext would re-interpret (the escapes above
introduce none of them, so the two passes cannot interfere); then make
every space a ~ -- a non-breaking space, exactly one character wide in
a monospace font, which LaTeX will not collapse -- so the code's
indentation and internal alignment survive.
"""
return quote_specials(escape_latex(line)).replace(" ", "~")
def widest_line(lines):
r"""The line to measure the block's width with.
Measured on the RAW lines: in a monospace font character count is
exact, whereas the escaped text is longer than it typesets
(\textbackslash{} is eleven characters and one glyph). The chosen
line is escaped afterwards, for \settowidth.
"""
return max(lines, key=len) if lines else ""
def tex_block(code, comment):
r"""One block: its lines, shaded if the block is annotated, beside its
comment. See the "Annotated code listings" section of sty/code.sty,
which owns the layout; this only supplies the three arguments."""
lines = code.strip("\n").split("\n")
if not any(line.strip() for line in lines):
return ""
# Every line is a plain \klline; whether the BLOCK is shaded is decided
# by \klcodebox from the comment, because the shading is one box around
# the block (that is what gives it vertical padding).
body = "\\\\\n".join(f"\\klline{{{tex_line(line)}}}" for line in lines)
return ("\\klblock{" + tex_line(widest_line(lines)) + "}{%\n"
+ body + "}{" + comment + "}\n")
def comment_text(comment):
"""The prose of a block comment: the // and the line count removed."""
return re.sub(r"^\s*//\d*\s*", "", comment.strip(), flags=re.S).strip()
html_escapes = {"&": "&amp;", "<": "&lt;", ">": "&gt;"}
html_escape_rgx = re.compile("[" + re.escape("".join(html_escapes)) + "]")
def html_line(text):
"""Code text as html: the markup characters escaped, then the
Klammertext specials quoted for the @eval read-back (same reason as
tex_line -- a raw # in "#include" would start a text removal). The
html entities introduce no Klammertext special, so the two passes
cannot interfere. No target :escape entries apply here, so each
quoted special decodes back to its own character."""
return quote_specials(html_escape_rgx.sub(
lambda m: html_escapes[m.group()], text))
def html_block(code, comment):
r"""One block: its lines beside its comment.
The layout is CSS (sks/code/css/code.css): .code_block is a flex row
with align-items center -- the same model as the LaTeX \parbox[c]
pair -- and .code_text is an inline-block with white-space: pre, so
its shrink-to-fit width IS the block's longest line and the shading
is one solid rectangle with no per-line work. Nothing is measured
here: unlike LaTeX, the browser does the layout.
"""
lines = code.strip("\n").split("\n")
if not any(line.strip() for line in lines):
return ""
shade = "code_border" if comment else "code_no_border"
result = f'<div class="code_text {shade}">{html_line(code.strip(chr(10)))}</div>'
if comment:
result += f'<div class="code_comment">{comment}</div>'
return f'<div class="code_block">{result}</div>\n'
class Code(klammer_base.Klammer_base):
id = 0
def __init__(self, K):
super().__init__(K)
self.text = phases.expand_whitespace_markers(self.text)
def annotated(self, pairs):
"""Does any block of this listing carry a comment?"""
return any(comment.strip() for _, comment in pairs)
def tex(self):
# No table: comments sit a fixed distance from their own block and
# deliberately do not align with each other, and each block's box is
# as wide as that block's longest line — so there is no column to
# align and nothing for a table to do. The layout is in
# sty/code.sty; see its "Annotated code listings" section.
#
# This is a RENDERER (final LaTeX, no klammers in the result), so
# the Klammermachine leaves it alone. It must never emit @code:
# a klammer that generates itself re-enters its own body with no
# base case, which the depth guard catches at 200 levels.
pairs = parse_blocks(split_blocks(self.text))
blocks = "".join(tex_block(code, comment_text(comment))
for code, comment in pairs)
listing = "\\begin{klcode}\n" + blocks + "\\end{klcode}\n"
captioned = bool(self.number or self.caption)
annotated = self.annotated(pairs)
inset = L.offset_length(self.offset)
# A caption does NOT by itself require a box, and boxing a listing
# costs page breaking -- a minipage cannot break, and listings are
# often long. Only two things genuinely need the shared box that
# add_caption builds:
#
# * a caption BESIDE the listing (:caption_side left or right),
# which has to know the listing's width;
# * an unannotated listing that must be centered or right-aligned,
# which has to be measured before it can be moved.
#
# Everything else is placed unboxed, and keeps breaking: the offset
# becomes \klshift/\klindent inside the environment, and the caption
# becomes a paragraph above or below, indented and width-matched to
# the listing. An annotated listing never needs measuring -- it
# spans the text column by construction.
beside = captioned and self.caption_side in ("left", "right")
must_measure = not annotated and (self.hpos != "left" or inset != "0pt")
if beside or must_measure:
return L.block(self.tex_boxed(listing, pairs, annotated,
captioned, inset))
return L.block(self.tex_unboxed(listing, annotated, captioned, inset))
def tex_boxed(self, listing, pairs, annotated, captioned, inset):
r"""The listing as a box: placed and captioned like a table or an
image, at the cost of not breaking across pages."""
measure = ""
if annotated:
# It spans the text column, so an offset narrows it rather than
# moving it; a centered element has no margin to be inset from.
width = ("\\linewidth" if self.hpos == "center"
else f"\\dimexpr\\linewidth-{inset}\\relax")
else:
# The \settowidth must stay OUTSIDE the minipage it sizes.
lines = [line for code, _ in pairs
for line in code.strip("\n").split("\n")]
measure = ("\\settowidth{\\kllistingwidth}{\\ttfamily "
+ tex_line(widest_line(lines)) + "}\n")
width = "\\kllistingwidth"
boxed = L.minipage(listing, width, vertical="t", center=False)
if captioned:
# add_caption attaches the caption to the box and hands the pair
# to caption_wrapper, so :pos and :offset move both together.
return measure + L.add_caption(
boxed, "Listing", self.number, self.caption, width,
hpos=self.hpos, side=self.caption_side,
font_symbol=self.caption_font,
font_size=self.caption_font_size, offset=self.offset)
return measure + L.caption_wrapper(boxed, self.hpos,
offset=self.offset)
def tex_unboxed(self, listing, annotated, captioned, inset):
r"""The listing in the running vertical list, so it can break across
pages. The offset is \leftskip plus a matching reduction of the
width \klblock computes its comment column from; the caption is a
paragraph on the same indent and width."""
shift = inset if self.hpos == "left" else "0pt"
# \hpos center cannot move a full-width listing, so it takes no
# offset -- the same rule the boxed path and caption_wrapper use.
indent = "0pt" if (annotated and self.hpos == "center") else inset
setup = (f"\\setlength{{\\klshift}}{{{shift}}}"
f"\\setlength{{\\klindent}}{{{indent}}}\n")
result = setup + listing
if captioned:
caption = L.make_caption_text(
self.number, "Listing", self.caption,
self.caption_font, self.caption_font_size)
# \nobreak: a caption must not be separated from its listing by
# a page break, even though the listing itself may break.
block = (f"\\noindent\\hspace*{{{shift}}}"
f"\\parbox[t]{{\\dimexpr\\linewidth-{indent}\\relax}}"
f"{{{caption}}}\\par")
if self.caption_side == "top":
result = block + "\\nobreak\n" + result
else:
result = result + "\\nobreak\n" + block + "\n"
return result
def html(self):
pairs = parse_blocks(split_blocks(self.text))
result = "".join(html_block(code, comment_text(comment))
for code, comment in pairs)
# The same two cases as tex_place, with the browser doing the work.
# An annotated listing's rows must stay full width so the comment
# column's flex: 1 has a remainder to take; an unannotated one gets
# width: fit-content, which is what lets the position container
# center or right-align it.
listing = E("div").cls("code_listing").body(result)
if not self.annotated(pairs):
listing.cls("code_listing_box")
# As in tex: the caption goes through the shared helper, so it sits
# in the same position container as the listing and moves with it.
if self.number or self.caption:
return html_util.add_caption(
listing, "Listing", self.number, self.caption,
self.caption_font, self.hpos, self.caption_side, True,
self.caption_font_size, offset=self.offset)
return html_util.hpos_container(listing, self.hpos, self.offset).str()
class Code_fragment(klammer_base.Klammer_base):
def __init__(self, K):
super().__init__(K)
def html(self):
#print(f"code: |{self.code_text}|")
result = self.code_text.strip()
result = undash(result)
#result = re.escape(result)
result = re.sub("<", "&lt;", result)
result = re.sub(" ", "&nbsp;", result)
#print(f"code: |{self.code_text}| -> |{result}|")
return f'<span class="code">{result}</span>'
def tex(self):
return f"{{\\tt {self.code_text.strip()}}}"
class Source(klammer_base.Klammer_base):
def __init__(self, K):
super().__init__(K)
with open(self.filename) as fp:
self.src = fp.read()
def tex(self):
result = self.src
# result = re.sub("#", "^#", result)
# result = re.sub("\\^", "\\^", result)
result = f"\\begin{{verbatim}}\n{result}\n\\end{{verbatim}}\n"
return result
def html(self):
result = escape_newlines(self.src.strip()) + "\n"
result = re.sub("@", "^@", result)
result = E("div").body(result).cls("code_text").str()
return result

View File

@@ -1,3 +1,5 @@
print("DEPRECATED")
if __name__ == "__main__":
import sys
sys.path.append("../kutil")
@@ -22,6 +24,49 @@ def escape_newlines(s):
# backslash_pat = re.compile(r'(".*?)\n(.*?")', re.S)
# return backslash_pat.sub(replace, s)
def is_comment(s):
return s.strip().startswith("//")
def split_blocks(s):
print('-'*40)
print(s)
print('-'*40)
blocks = []
in_code = True
block = ""
for line in s.rstrip().split("\n"):
if is_comment(line):
if in_code:
blocks.append(block)
block = line + "\n"
in_code = False
else:
block += line + "\n"
else:
if not in_code:
blocks.append(block)
block = line + "\n"
in_code = True
else:
block += line + "\n"
if block:
blocks.append(block)
for block in blocks:
print("B:")
print(block)
return blocks
def parse_blocks(blocks):
box_comment_rgx = re.compile("\s*//(\d+)\s+.*", re.S)
i = 0
while i < len(blocks):
match = box_comment_rgx.match(blocks[i])
if match:
print(match.group(1))
i += 1
def get_blocks(s):
comment_pat = re.compile(r"(\s*)//(\d+)\s+(.*)", re.S)
blocks = []
@@ -134,7 +179,27 @@ class Code(klammer_base.Klammer_base):
s = s.replace(">", "\\textgreater{}")
return s
def code_box(self, text):
kutil.msg(text)
result = ""
for line in text.rstrip().split("\n"):
indent = len(line) - len(line.lstrip())
eline = ("~" * indent) + line[indent:]
print(indent, line)
print(eline)
result += eline + "\\\\\n"
result = result[:-3]
print("RESULT:")
print(result)
return result
def tex(self):
parse_blocks(split_blocks(self.text))
return ""
# Escape Klammertext special characters
self.text = self.text.replace("^", "^^")
self.text = self.text.replace("#", "^#")
@@ -153,6 +218,14 @@ class Code(klammer_base.Klammer_base):
result = ""
count = len(self.blocks)
for text, comment in self.blocks:
print("TEXT:")
print(text)
print("COMMENT:")
print(comment)
text = " " + re.sub("\n", " \n ", text) + " "
longest = longest_line(text)
text = latex_spaces(text)
@@ -160,11 +233,23 @@ class Code(klammer_base.Klammer_base):
text = f"{start_strut}\\ttfamily {text}{end_strut}"
width = f"\\widthof{{\\ttfamily {longest}}}"
code = L.environment("minipage", text, width) + "\\\\\n"
print(code)
#code = "\\asymbox{" + self.code_box(text) + "}"
#print(code)
code = text
if comment:
width = f"\\linewidth - {width} - {indent} - {comment_sep}"
#width = f"\\linewidth - {width} - {indent} - {comment_sep}"
"""
width = f"\\linewidth - \widestline - {indent} - {comment_sep}"
code = f"\\fcolorbox{{Gray}}{{LightGray}}{{{code}}}"
code += f"\\rule{{{comment_sep}}}{{{strutvis}}}" \
+ L.environment("minipage", "\\sffamily\\small\\raggedright " + comment, width)
"""
code = "\\asymbox{" + self.code_box(text) + "}" + comment
result += f"\\rule{{{indent}}}{{{strutvis}}}{code}"
if comment:
if i < count - 1 and self.blocks[i+1][1]:

View File

@@ -2,12 +2,36 @@
font-family: var(--monospace);
}
/* A whole listing, inside its :hpos position container. Full width by
default so an annotated listing's comment column has a remainder to take
(.code_comment is flex: 1); an UNANNOTATED listing is shrink-to-fit
instead, which is what lets :hpos center or right-align it -- a
full-width child cannot be moved within its container. The LaTeX
counterpart is Code.tex_place. */
.code_listing {
}
.code_listing_box {
width: fit-content;
}
/* An annotated code listing (sks/code/code_block.py). The same model as
the LaTeX side in sty/code.sty: each BLOCK is a run of code lines with
an optional comment beside it, the shaded box is as wide as that block's
longest line, and comments sit a fixed distance from their own block
without aligning with each other.
align-items: center is the counterpart of LaTeX's \parbox[c] pair. No
vertical margin or padding anywhere in the stack: consecutive blocks
must abut exactly, so that adjacent shaded blocks read as one region and
the line rhythm is the same whether a line is shaded or not. That is
what \strut and \offinterlineskip buy on the LaTeX side; here it is just
the absence of vertical space. */
.code_block {
display: flex;
align-items: center;
margin: .125rem 0 0 1rem;
margin: 0 0 0 1rem;
padding: 0;
/* flex-direction: column-reverse; */
}
.code_caption {
@@ -15,20 +39,28 @@
font-style: italic;
}
/* white-space: pre keeps the code's own spacing, and the shrink-to-fit
width of an inline-block IS the block's longest line — so the shading is
one solid rectangle with no per-line work and nothing measured. Padding
is horizontal only; see the note on .code_block. */
.code_text {
display: inline-block;
/* vertical-align: top; */
white-space: pre;
font-family: var(--monospace);
line-height: 1.2;
line-height: 1.4;
/* No horizontal padding: the box's left edge sits ON the code column,
where the line's own indentation begins, rather than out to the left
of it. LaTeX counterpart: \klcodepad 0pt. */
padding: 0;
}
/* The comment takes what the block leaves of the containing width — the
browser's counterpart of \dimexpr\linewidth-\klcodewidth-\klcodegap.
min-width is the counterpart of \klcodemin: below it the comment would
be too narrow to set prose in. */
.code_comment {
display: inline-block;
/* vertical-align: top; */
padding: .25rem;
border: solid white 1px;
padding: .25rem .25rem .25rem .5rem;
flex: 1;
padding-left: .75rem;
font-family: var(--sans-serif);
font-style: italic;
font-size: .8rem;
@@ -36,22 +68,23 @@
line-height: 1.25;
}
/* An annotated block: background only, no border. A border would need a
matching one on the unshaded blocks to keep the code aligned, and the
design settled on shading alone. */
.code_border {
border: solid gray 1px;
/* margin: .125rem; */
margin: .125rem .125rem .125rem .25rem;
padding: .125rem .5rem .25rem .5rem;
background-color: rgb(95%,95%,95%);
background-color: rgb(100%,100%,60%);
/* Clear space above and below a boxed block, so two boxed blocks with
no unboxed lines between them read as two boxes rather than one.
On the block, never on the line: the lines within a block must still
abut, or the shading stops being a solid rectangle. The LaTeX
counterpart is \klboxgap in sty/code.sty. */
margin: 2px 0;
/* The box's own vertical margin: space inside the shading, above the
first line and below the last. LaTeX counterpart: \klboxpad. */
padding-top: 2px;
padding-bottom: 2px;
}
.code_no_border {
padding: .125rem 0 .125rem .5rem;
margin: 0 0 0 .125rem;
border: solid white 1px;
/* Debugging:
border: solid lightgray 1px;
background-color: rgb(250,250,127);
*/
}

View File

@@ -7,3 +7,168 @@
\usepackage[strings,nohyphen]{underscore}
\usepackage{mdframed}
\newsavebox{\measurebox}
\newlength{\widestline}
% \measurewidest{line one\\line two\\...} -> \widestline = width of widest
\newcommand{\measurewidest}[1]{%
\begin{lrbox}{\measurebox}%
\begin{tabular}{@{}l@{}}#1\end{tabular}%
\end{lrbox}%
\setlength{\widestline}{\wd\measurebox}%
}
% \asymbox[<left pad>]{line one\\line two\\...} other three sides: 8pt
\newcommand{\asymbox}[2][0pt]{%
\begin{lrbox}{\measurebox}%
\begin{tabular}{@{}l@{}}\tt #2\end{tabular}%
\end{lrbox}%
{\setlength{\fboxsep}{0pt}%
\fcolorbox{black}{yellow!20}{%
\kern#1%
\vbox{\kern0pt\hbox{\usebox{\measurebox}}\kern0pt}%
\kern0pt}}%
}
% ===========================================================================
% Annotated code listings. Emitted by sks/code/code_block.py; the layout
% policy lives here so it can be tuned without touching the Python.
%
% The model: a listing is a vertical stack of BLOCKS, each block a run of
% code lines with an optional comment beside it. There is deliberately no
% table -- comments are a fixed distance from their own block and do not
% align with each other, and each block's shaded box is as wide as that
% block's longest line.
%
% Two mechanisms carry the whole appearance:
%
% 1. Every line is a \strut'ed \makebox of the block's width. A LaTeX
% \strut is exactly 0.7\baselineskip high and 0.3 deep -- one full
% \baselineskip -- so consecutive lines inside a block abut exactly,
% with no gap and no overlap, at any point size. Without the strut a
% line with no descender would sit closer to its neighbour and the
% rhythm would follow the text rather than the grid.
%
% 2. \offinterlineskip in the listing removes the interline glue BETWEEN
% BLOCKS, so consecutive blocks abut too and a boxed block continues
% the rhythm of the plain lines around it. (It does not reach inside
% a \parbox, which restores normal interline spacing -- that is why
% the struts in point 1 are doing the work there, and why the box's
% vertical padding comes from \fboxsep below rather than from a
% padding line, which would be spaced off the grid.)
% ===========================================================================
\definecolor{klcodeshade}{rgb}{1,1,.6}
\newlength{\kllistingwidth} % width of a whole unannotated listing
% How the listing is placed WITHOUT being boxed, so that it can still break
% across pages: \klshift moves it right (an :offset with :hpos left) and
% \klindent is the width taken out of \linewidth (either side's offset).
% A minipage would do both at once but cannot break -- see Code.tex in
% sks/code/code_block.py, which sets these before \begin{klcode}.
\newlength{\klshift} \setlength{\klshift}{0pt}
\newlength{\klindent} \setlength{\klindent}{0pt}
\newlength{\klcodewidth} % width of the current block's box
\newlength{\klcommentwidth} % what is left for its comment
% Horizontal inset of the code from the box's left edge. 0pt puts the
% box's left edge ON the code column, where the line's own indentation
% begins, rather than out to the left of it; a positive value moves the
% CODE right, never the box left.
\newlength{\klcodepad} \setlength{\klcodepad}{0pt}
\newlength{\klcodegap} \setlength{\klcodegap}{14pt} % box to comment
\newlength{\klcodemin} \setlength{\klcodemin}{6em} % narrower: see below
% Clear space above and below a block that HAS a box, so that two boxed
% blocks with no unboxed lines between them read as two boxes rather than
% one. Applied per block, never per line: the lines within a block must
% still abut, or the shading stops being a solid rectangle.
% 1.5pt is 2px at the CSS reference 96dpi, matching the html margin in
% css/code.css -- these two are meant to look the same, so change them
% together.
\newlength{\klboxgap} \setlength{\klboxgap}{1.5pt}
% The box's own vertical margin: space INSIDE the shading, above the first
% line and below the last. Same 2px equivalent, matching the html padding.
\newlength{\klboxpad} \setlength{\klboxpad}{1.5pt}
% \klline{<line>} — one code line, padded and strut'ed to the block width.
\newcommand{\klline}[1]{%
\makebox[\klcodewidth][l]{\hspace{\klcodepad}\strut\ttfamily #1}}
% \klcodebox{<lines>} — the block's lines, shaded when the block has a
% comment (\klcomment is set by \klblock).
%
% The shading is ONE \colorbox around the whole block, not one per line,
% because only a box around the whole block can have a vertical margin.
%
% That margin is \vspace* INSIDE the \parbox rather than \fboxsep: \fboxsep
% pads all four sides, and any horizontal padding would put the box's left
% edge out to the LEFT of the code column. With \fboxsep 0 the box spans
% exactly the code's own extent, so it begins where the line's indentation
% begins and the code stays aligned with the unshaded lines around it.
\newcommand{\klcodebox}[1]{%
\ifx\klcomment\empty
\parbox[c]{\klcodewidth}{#1}%
\else
\setlength{\fboxsep}{0pt}%
\colorbox{klcodeshade}{%
\parbox[c]{\klcodewidth}{\vspace*{\klboxpad}#1\vspace*{\klboxpad}}}%
\fi}
% \klblock{<widest line>}{<lines>}{<comment>}
% <lines> is \klline/\klshaded calls separated by \\; <comment> may be
% empty. The comment gets what the box leaves of \linewidth (NOT
% \textwidth: inside a list or minipage they differ, and \textwidth would
% push the comment into the margin). If that remainder is too narrow to
% set prose in, the comment goes BELOW the block rather than being
% squeezed into an overfull box.
\newcommand{\klblock}[3]{%
\begingroup
% A block is boxed exactly when it has a comment, so this one test also
% decides whether the block gets the \klboxgap separation.
\def\klcomment{#3}%
\ifx\klcomment\empty\else\vskip\klboxgap\fi
\settowidth{\klcodewidth}{\ttfamily #1}%
\addtolength{\klcodewidth}{2\klcodepad}%
% The box advances the line by exactly \klcodewidth (\fboxsep is 0 in
% \klcodebox). If a horizontal padding is ever reintroduced there, its
% width must be subtracted here too: leaving it out overfills the line,
% and \raggedright then breaks it -- dropping the comment onto the next
% line at the margin, which looks like the narrow-comment fallback.
\setlength{\klcommentwidth}%
{\dimexpr\linewidth-\klindent-\klcodewidth-\klcodegap\relax}%
\noindent
\ifdim\klcommentwidth<\klcodemin
\klcodebox{#2}%
\ifx\klcomment\empty\else
\\\parbox[t]{\linewidth}{\klcommentfont #3}%
\fi
\else
\klcodebox{#2}%
\ifx\klcomment\empty\else
\hspace{\klcodegap}%
\parbox[c]{\klcommentwidth}{\klcommentfont #3}%
\fi
\fi
% End the block's line. Without this every block joins ONE horizontal
% list and the blocks are broken into lines and justified like words.
\par
\ifx\klcomment\empty\else\vskip\klboxgap\fi
\endgroup}
% The annotation font. Ragged right: a justified annotation beside a
% narrow box hyphenates badly.
\newcommand{\klcommentfont}{\sffamily\itshape\small\raggedright}
% \begin{klcode} ... \end{klcode} — the listing itself. \offinterlineskip
% is what makes the struts the only thing setting vertical rhythm.
\newenvironment{klcode}
{\par\addvspace{0.5\baselineskip}%
\begingroup
\setlength{\parindent}{0pt}%
\setlength{\parskip}{0pt}%
\raggedright
% An unboxed listing is shifted with \leftskip rather than wrapped in a
% minipage, so that it can still break across pages.
\leftskip=\klshift
\offinterlineskip}
{\endgroup\par\addvspace{0.5\baselineskip}}

View File

@@ -17,51 +17,45 @@
# @@image.k basename : Image read from a file @@
# @@image.html basename | width | height : @eval import image ; result = image.image(K) eval@ @@
@@@argtype image_hpos |
horizontal position of an image: the element positions (center, left,
right, or a length used as the left margin), or none for an inline image
with no positioning container
:pattern 'element_hpos'^|none
:default center
@@@
# An image once had its own position type, for the sake of the "none" value
# (no positioning container). That value is meaningful for any block
# element, so the shared hpos type carries it and an image is placed by the
# hpos_args option set like every other block element.
@@image
@@image.k
basename
:id
:width.length .5w
@caption_arguments@
@caption_args@
:vmargin.bool true
:hpos.image_hpos
@hpos_args@
:rel
:abswidth.number 0.0
:border.bool false
:
@eval image.Image(K) eval@
An image read from a file. The basename names the file, which is looked
for in the image search path and converted to a format the target can use.
^:width is the image's width in the text column, ^:hpos and ^:offset place
it there, and the caption arguments caption it.
@@
@@image_grid
@@image :: @eval image.Image(K) eval@ @@
@@image_grid.k
image_specs.rest(2)
:caption
:number.bool true
:cell_number.bool false
:landscape.bool false
:scale.number 0.98
:caption_side.caption_side
:caption_side_center.bool true
#
@caption_args@
@hpos_args@
:thumbnail.bool false
:allow_break.bool true
# :indent.length
# :xmargin.length
:id
:captionfont
:caption_width.float .9
:rel
:hsep.number 0.02
:
@eval image_grid.Image_grid(K) eval@
: A grid of images
@@
@@fig spec.figure_id :
@reference *spec* | Figure @
@@
@@image_grid :: @eval image_grid.Image_grid(K) eval@ @@
@@fig spec.figure_id : @reference *spec* | Figure @ @@

View File

@@ -103,7 +103,8 @@ class Image(klammer_base.Klammer_base):
result = html_util.add_caption(
result, "Figure", self.number, self.caption, self.caption_font,
self.hpos, self.caption_side, False, self.caption_font_size)
self.hpos, self.caption_side, False, self.caption_font_size,
offset=self.offset)
result = E("div").body(result).cls("image_margin")
@@ -131,16 +132,19 @@ class Image(klammer_base.Klammer_base):
self.caption += self.file_error_message
result = latex_util.add_caption(
result, "Figure", self.number, self.caption, width,
self.hpos, self.caption_side)
self.hpos, self.caption_side, offset=self.offset)
#result, "Figure", self.number, self.caption, self.hpos, self.caption_side,
#self.width, self.caption_side_center, self.vmargin, self.caption_margin)
else:
result = f'\\includegraphics[width={width}]{{{source}}}'
result = latex_util.caption_wrapper(result, self.hpos)
result = latex_util.caption_wrapper(result, self.hpos, offset=self.offset)
name = f"Reference-Figure-{Image.id}"
result = f"\\hypertarget{{{name}}}{{}}\\label{{Label-{name}}}\n{result}"
Image.id += 1
return result
# \includegraphics (bare or inside a caption wrapper's minipage) is
# box material: it must sit in vertical mode or it is typeset beside
# any text it follows.
return latex_util.block(result)
def txt(self):

View File

@@ -20,7 +20,13 @@ class Kargs:
self.caption_side_center = False
self.Image_search_path = K.Image_search_path
self.number = K.cell_number
# A cell is placed by the grid, not by itself: no positioning
# container, and nothing to inset it from. Both members of the
# hpos_args option set have to be set here -- image.py reads them
# unconditionally, so a missing one is an AttributeError at render
# time rather than a message.
self.hpos = "none"
self.offset = "0pt"
self.K_target = target
self.K_input_dir = K.K_input_dir
self.K_output_dir = K.K_output_dir

View File

@@ -1,22 +1,6 @@
@@show s : @eval :cpp show show @ @@
# General definitions for the Standard Klammer Set
@@@argtype caption_side |
the side of its element on which a caption is placed
:pattern top^|right^|bottom^|left
:default bottom
@@@
@@caption_arguments :
:caption
:number.bool true
:caption_side.caption_side
:caption_font.font i
:caption_font_size.float .9
@@
@@reference spec | name :
__REF__*spec*__*name*__
@@
# Argtypes
@@@argtype number | a number
:pattern 'float'^|'int'
@@ -24,9 +8,9 @@
@@@
@@@argtype length | a length specifier
#:pattern f^|none^|'float'w^|'float'h^|'int'px^|'float'em^|'int'pt
# The last pattern is a string, used where possible to determine its length
:pattern f^|none^|'float'w^|'float'h^|'int'px^|'float'em^|'int'pt^|"[^^"]+"^|'float'pw^|'float'ph
#:pattern f^|none^|'float'w^|'float'h^|'int'px^|'float'em^|'int'pt
# The last pattern is a string, used where possible to determine its length
:pattern f^|none^|'float'w^|'float'h^|'int'px^|'float'em^|'int'pt^|"[^^"]+"^|'float'pw^|'float'ph
# :python_cast (lambda s : __import__("kutil").parse_length("tex", s))
@@@
@@ -35,16 +19,49 @@
#:python_cast (lambda s : [__import__("kutil").parse_length("tex", e) for e in s.split()])
@@@
@@@argtype element_hpos |
the horizontal position of a block element (a table or an image) within
the text column: center, left, right, or a length, which places the
element's left edge that far from the left margin (e.g. ^:hpos 4em, ^:hpos
.25w). When the element is as wide as the text column, the positions are
indistinguishable.
:pattern center^|left^|right^|'length'
@@@argtype side |
the side of its element on which a caption is placed: top, right, bottom, left
:pattern top^|right^|bottom^|left
:default bottom
@@@
@@@argtype hpos |
a horizontal position: left, center, right, none
:pattern left^|center^|right^|none
:default center
@@@
@@@argtype offset_length |
how far a block element is inset from the margin that ^:hpos names: from
the left margin for ^:hpos left, from the right margin for ^:hpos right.
An offset has no meaning for a centered element and is ignored there.
Written alone, ^:offset is the standard indentation (e.g. ^:hpos left
^:offset), and a value overrides it (^:hpos right ^:offset 4em).
# A single-purpose type, not a use of "length", because the two values
# below are what the writer relies on and only a type can carry them: an
# element is flush unless an offset is asked for (:default), and asking
# without saying how much is the standard indentation (:alone). A general
# length type must not declare :alone -- a bare option name has to read the
# same way wherever it appears, and "2em" is meaningless for :width.
:pattern 'length'
:default 0pt
:alone 2em
@@@
#[
@@@argtype element_hpos |
the horizontal position of a block element (a table, an image, a code
listing) within the text column: center, left, right, or a length, which
places the element's left edge that far from the left margin (e.g. ^:hpos
4em, ^:hpos .25w). When the element is as wide as the text column, the
positions are indistinguishable. The value none puts the element in no
positioning container at all, so that it flows with the text around it.
:pattern center^|left^|right^|none^|'length'
:default center
@@@
]#
@@@argtype figure_id |
an identifier for a figure.
@@ -89,8 +106,75 @@ An <id> is the value of the ^:id argument for an image.
:pattern [a-z][a-z]
@@@
# The document-wide language for generated text. A klammer's own :lang
# argument overrides it; see the language argtype above. Consumers today:
# @date and @datetime (month names and date form). Set it for a whole
# document with @@@state Language :value de @@@
@@@state Language :desc Language (ISO 639-1) for generated text :value en @@@
# State variables
@@@state Language :desc Language (ISO 639-1) for generated text.
The document-wide language for generated text. A klammer's own ^:lang
argument overrides it; see the language argtype above. Currently used by
^@date and ^@datetime (month names and date form). Set it for a whole
document with ^@^@^@state Language ^:value de ^@^@^@
:value en
@@@
# Option sets
#[
The horizontal placement of a block element is two parameters: where it sits
(hpos_arg) and how far it is inset from that side (offset_arg). They are two
sets rather than one because a klammer may need the second without the first
-- and because a set is a vocabulary a reader learns whole, so a small one is
easier to learn than a large one.
An option set is a claim about behavior, not just a saving of keystrokes: a
klammer whose declaration uses these must also POSITION itself with them,
through latex_util.caption_wrapper (tex) and html_util.hpos_container
(html), or the parameter is accepted and silently ignored. Asserted by
outcome in sks/tst/placement_test.sh, so a new block klammer that takes the
parameters and ignores them is caught.
A klammer that wants a different DEFAULT position says so where it uses the
set (@hpos_arg :hpos left @ in the code klammer): a set owns the names and
the types, and the klammer owns what silence means for it.
]#
#[
@@offset_arg.o
:offset.offset_length
: How far a block element is inset from the margin its position names @@
@@hpos_arg.o
:hpos.element_hpos
: Where a block element sits in the text column @@
]#
@@hpos_args.o
:hpos.hpos
:offset.offset_length
:
The horizontal position of a block element. If the ^:hpos value is "none", no
outer structure is added to the element so that it can be used inline or in
other structures. The ^:offset is used only if the ^:hpos value is "left" or
"right".
@@
@@caption_args.o
:caption
:number.bool true
:caption_side.side
:caption_font.font i
:caption_font_size.float .9
: Arguments that define a caption for a block element.
@@
# Klammers
@@reference.k spec | name : Reference marker for captioned elements @@
@@reference ::
__REF__*spec*__*name*__
@@
@@show.k s : Show the raw Klammertext and the result @@
@@show :: @eval :cpp show show @ @@

View File

@@ -193,10 +193,10 @@
@@rowcolor.tex s : \colorrow{*s*} @@
@@table rows.rest(2)
@@table.k rows.rest(2)
:id
@caption_arguments@
:hpos.element_hpos
@caption_args@
@hpos_args@
:header.bool true
:allow_break.bool false
:column_width.column_width
@@ -215,9 +215,20 @@
:leading.float 1.3
:colsep 4pt
:
@eval table.Table(K) eval@
A table of rows of cells. The cells of a row are separated by "|" and the
rows by "||", so the argument is two-dimensional; the first row is the
header unless ^:header is false.
Everything else is optional, and a table written with no options is a plain
grid of its cells. ^:column_width lays the columns out, ^:hline and ^:vline
draw lines, ^:colspan and ^:rowspan merge cells, ^:justify and ^:cell_hpos
place text within them, ^:calc computes cells from other cells and ^:format
formats them, and ^:hpos and ^:offset place the whole table in the text
column.
@@
@@table :: @eval table.Table(K) eval@ @@
@@tbl spec.figure_id :
@reference *spec* | Table @
@@

View File

@@ -591,10 +591,11 @@ class Table(klammer_base.Klammer_base):
result, "Table", self.number, self.caption,
self.caption_font, hpos=self.hpos,
side=self.caption_side,
font_size=self.caption_font_size, max_width="100%")
font_size=self.caption_font_size, max_width="100%",
offset=self.offset)
else:
result.attr("style", "max-width: 100%")
result = html_util.hpos_container(result, self.hpos).str()
result = html_util.hpos_container(result, self.hpos, self.offset).str()
return result
colgroup, layout, table_width = self.html_colgroup()
result = E("table").body(colgroup + result)
@@ -607,13 +608,14 @@ class Table(klammer_base.Klammer_base):
result = html_util.add_caption(
result, "Table", self.number, self.caption, self.caption_font,
hpos=self.hpos, side=self.caption_side,
font_size=self.caption_font_size, width=table_width)
font_size=self.caption_font_size, width=table_width,
offset=self.offset)
else:
# An uncaptioned table still gets the position container, so
# html and tex agree on where the table sits.
if table_width:
result.attr("style", f"{layout}width: {table_width}")
result = html_util.hpos_container(result, self.hpos).str()
result = html_util.hpos_container(result, self.hpos, self.offset).str()
return result
# LaTeX
@@ -768,14 +770,25 @@ class Table(klammer_base.Klammer_base):
# by its :hpos wrapper instead; its glue is left neutral (\fill on
# both sides collapses in the exactly-fitting box), because a fixed
# length would overflow the box. A length is the left margin.
# :offset insets the table from the margin :hpos names -- the same
# rule the boxed path gets from latex_util.caption_wrapper, here
# expressed as the fixed side of the glue pair.
inset = latex_util.offset_length(self.offset)
if not self.allow_break:
left, right = "\\fill", "\\fill"
elif self.hpos == "center":
left, right = "\\fill", "\\fill"
elif self.hpos == "left":
left, right = "0pt", "\\fill"
left, right = inset, "\\fill"
elif self.hpos == "right":
left, right = "\\fill", "0pt"
left, right = "\\fill", inset
elif self.hpos == "none":
# No positioning container: the table starts at the text margin
# like ordinary text, which is what "none" does in html (the
# hpos_none class is inline-flex). A longtable cannot flow
# inline, so flush left is as close as the target gets. The
# offset names no margin here and is ignored, as when centered.
left, right = "0pt", "\\fill"
else:
length, _, _ = kutil.parse_length("tex", self.hpos, 1)
left, right = length, "\\fill"
@@ -824,9 +837,9 @@ class Table(klammer_base.Klammer_base):
result, "Table", self.number, self.caption, "\\tableboxwidth",
hpos=self.hpos, side=self.caption_side,
font_symbol=self.caption_font,
font_size=self.caption_font_size)
font_size=self.caption_font_size, offset=self.offset)
else:
result = latex_util.caption_wrapper(result, self.hpos)
result = latex_util.caption_wrapper(result, self.hpos, offset=self.offset)
result = measure + result
result = f"\\hypertarget{{{name}}}{{}}\\label{{Label-{name}}}\n{result}"
@@ -845,7 +858,11 @@ class Table(klammer_base.Klammer_base):
+ self.tex_fill_widths()
+ result)
result = re.sub(r"\newline", r"\\\\", result)
return result
# A boxed table is box material: it must sit in vertical mode or it
# is typeset beside any text it follows. (A page-breaking longtable
# breaks the paragraph itself, but \par on both sides is a no-op
# there, so the rule stays unconditional.)
return latex_util.block(result)
def txt(self):
return "Table in .txt format not implemented"

View File

@@ -168,6 +168,14 @@
justify-content: left;
}
/* Retired 2026-08-03: this fixed 1em on every left- and right-placed
element was the :offset argument, hard coded and only in html -- the tex
side had no counterpart, so the two targets disagreed. It is now the
offset_length argtype (sks/kutil/kutil.k), applied by
html_util.hpos_container and latex_util.caption_wrapper. A document
that wants the old inset writes ":offset", whose alone value is the
standard indentation.
.hpos_margin {
margin: 0 1em 0 1em;
}
*/

View File

@@ -193,27 +193,38 @@ def element_tag(element):
def font_class(font):
return {"r" : "", "i" : "ritalic", "t" : "monospace", "s" : "sanserif"}[font]
def hpos_container(element, hpos):
def hpos_container(element, hpos, offset=None):
# Wrap element in its horizontal-position container. hpos is left,
# center, right, none, or a length, which becomes the left margin
# (the tex counterparts are the \LTleft glue for tables and the
# \hspace* in latex_util.caption_wrapper).
style = None
#
# offset insets the element from the margin hpos names, as padding on
# that side of the flex container: the left for "left", the right for
# "right". A centered element has no such margin, so the offset is
# ignored there rather than being an error -- same rule as
# latex_util.caption_wrapper, which is the tex counterpart. (It
# replaces the fixed 1em that the hpos_margin class used to add to
# every left- and right-placed element: that was this offset, hard
# coded, and only in html.)
styles = []
if hpos not in ("left", "center", "right", "none"):
length, _, _ = kutil.parse_length("html", hpos, 1)
style = f"margin-left: {length}"
styles.append(f"margin-left: {length}")
hpos = "indent"
if offset and offset not in ("0", "0pt") and hpos in ("left", "right", "indent"):
length, _, _ = kutil.parse_length("html", offset, 1)
side = "right" if hpos == "right" else "left"
styles.append(f"padding-{side}: {length}")
result = E("div").cls("hpos_" + hpos).body(element)
if style:
result.attr("style", style)
if hpos not in ("center", "indent"):
result.cls("hpos_margin")
if styles:
result.attr("style", "; ".join(styles))
return result
def add_caption(element, caption_label, number, caption_text,
font_symbol="i", hpos="center", side="bottom", as_string=True,
font_size=.9, width=None, max_width=None):
font_size=.9, width=None, max_width=None, offset=None):
tag = element_tag(element)
# Caption
caption = ""
@@ -263,7 +274,7 @@ def add_caption(element, caption_label, number, caption_text,
element.attr("style", f"max-width: {max_width}")
result = hpos_container(element, hpos)
result = hpos_container(element, hpos, offset)
if number:
result.cls("element_container")

View File

@@ -12,6 +12,31 @@ def tex_style():
# \usepackage{quoting}
def block(material):
r"""Return tex material as a block: vertical mode on both sides.
A block element -- a table, an image, a code listing -- is box
material. Appended to a non-empty horizontal list (an @image written
after text on the same line, with no blank line between) a box is
typeset BESIDE the text rather than below it, and text following it
flows to its right. A \par on each side ends the paragraph in
progress and starts a new one after. \par in vertical mode is a
no-op, so this is safe wherever it is applied: a block klammer needs
no knowledge of what preceded it.
Every SKS klammer whose tex output is box material returns it through
this function. LaTeX ENVIRONMENTS -- center, flushright, quote,
itemize, verbatim -- already break the paragraph themselves and do
not need it.
The paragraph policy this serves: in an @document, paragraphs are
made from blank-line-separated text (@par is the explicit form for a
fragment rendered without @document), and the author is never
required to know that a target distinguishes horizontal from
vertical mode. Asserted by sks/tst/paragraph_test.sh.
"""
return "\\par\n" + material.strip("\n") + "\n\\par\n"
def environment(name, body, required=None, optional=None):
req = f"{{{required}}}" if required else ""
opt = f"[{optional}]" if optional else ""
@@ -54,17 +79,36 @@ def minipage(content, width="\\textwidth", vertical="c", center=True, vmargin=""
result = f"\\fbox{{{result}}}"
return result
def caption_wrapper(element, hpos, bottom_margin=.67):
def offset_length(offset):
r"""An :offset as a LaTeX length, or "0pt" when there is none."""
if not offset or offset in ("0", "0pt"):
return "0pt"
return kutil.parse_length("tex", offset, 1)[0]
def offset_space(offset):
r"""An :offset as an \hspace*, or "" when there is none."""
length = offset_length(offset)
return "" if length == "0pt" else f"\\hspace*{{{length}}}"
def caption_wrapper(element, hpos, bottom_margin=.67, offset=None):
# hpos is left, center, right, none (no wrapper), or a length, which
# becomes the left margin. The element is a box on a line inside a
# full-width minipage; \hfill on the empty side pushes it into place.
#
# offset insets the element from the margin hpos names -- from the left
# for "left", from the right for "right". A centered element has no
# such margin, so the offset is ignored there rather than being an
# error: the two arguments are independent, and :hpos is what decides
# whether the offset has anything to measure from. (The html
# counterpart is html_util.hpos_container.)
vmargin = f"{bottom_margin}\\baselineskip"
inset = offset_space(offset)
if hpos == "center":
return minipage(element, vmargin=vmargin)
if hpos == "left":
return minipage(element + "\\hfill", vmargin=vmargin, center=False)
return minipage(inset + element + "\\hfill", vmargin=vmargin, center=False)
if hpos == "right":
return minipage("\\hfill" + element, vmargin=vmargin, center=False)
return minipage("\\hfill" + element + inset, vmargin=vmargin, center=False)
if hpos == "none":
return element
length, _, _ = kutil.parse_length("tex", hpos, 1)
@@ -85,7 +129,8 @@ def make_caption_text(number, label, text, font_symbol, font_size):
return caption
def add_caption(element, caption_label, number, caption_text, latex_width,
hpos="center", side="bottom", font_symbol="i", font_size=.9):
hpos="center", side="bottom", font_symbol="i", font_size=.9,
offset=None):
top_margin = .75 if "includegraphics" in element else .5
caption = make_caption_text(number, caption_label, caption_text, font_symbol, font_size)
if caption:
@@ -118,4 +163,4 @@ def add_caption(element, caption_label, number, caption_text, latex_width,
# space below its line.
element = minipage(caption + "\\rule[-0.75\\baselineskip]{0pt}{0pt}\n" + element,
latex_width, vertical="t")
return caption_wrapper(element, hpos)
return caption_wrapper(element, hpos, offset=offset)