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,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}}