katom types from ref/ in groups, @table :leading in lines of text and :rulesep, @code under txt, one table style attribute, literal-klammer options (from dev 2e453744f5e9)
kdesc --katoms is Klammertext rendered from ref/, the types in titled groups, with --katoms full for the regex column. @table :leading L is L lines of the surrounding text in html as in pdf (default 1), and :rulesep spaces the horizontal rules. The html table has one style attribute, so :column_width fractions take effect again. @code and @source_listing render in plain text; captions are numbered per kind there, and a table needs no blank line around it. @c takes :nd, and an option-shaped word before a bar inside literal code is code. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
63956c00e5
commit
217c0f3fc5
23 files changed
+625
-290
No files matched your search
+4
-3
@@ -13,15 +13,16 @@ katom when no bar is written, "^@code :filename f :pattern p" DISPLAYED THOSE
|
||||
WORDS as the listing rather than acting on them.
|
||||
@@
|
||||
|
||||
@@code.html,tex :: @eval code_block.Code(K) @ @@
|
||||
@@code.html,tex,txt :: @eval code_block.Code(K) @ @@
|
||||
|
||||
@@c.k code_text.literal :
|
||||
@@c.k :nd.bool | code_text.literal :
|
||||
A word or phrase displayed verbatim in a line. Like ^@code, its content is
|
||||
LITERAL -- nothing in it is interpreted as Klammertext -- so it must be closed
|
||||
with the named delimiter "c^@". ^@c is the in-line form and ^@code the block
|
||||
form of the same thing; before 2026-08-16 this parameter was an ordinary
|
||||
string, so a "^#" or a bare "^@" inside it was read as Klammertext and usually
|
||||
failed the file, which is not what "verbatim" can mean.
|
||||
If :nd (no delimiters) is true, then the txt target delimiters are excluded.
|
||||
@@
|
||||
|
||||
@@c.html,tex,txt :: @eval code_block.Code_fragment(K) eval@
|
||||
@@ -44,4 +45,4 @@ quietly different listing.
|
||||
|
||||
Named "^@source_file" until 2026-08-16. # retired-ok
|
||||
@@
|
||||
@@source_listing.html,tex :: @eval :cwd *K_input_dir* code_block.Source(K) @ @@
|
||||
@@source_listing.html,tex,txt :: @eval :cwd *K_input_dir* code_block.Source(K) @ @@
|
||||
+73
-7
@@ -5,11 +5,13 @@
|
||||
|
||||
import re
|
||||
import klammer_base
|
||||
import kutil
|
||||
import html_util
|
||||
import txt_util
|
||||
import phases
|
||||
from html_util import E
|
||||
import latex_util as L
|
||||
|
||||
|
||||
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.
|
||||
@@ -119,7 +121,13 @@ def tex_line(line):
|
||||
a monospace font, which LaTeX will not collapse -- so the code's
|
||||
indentation and internal alignment survive.
|
||||
"""
|
||||
return quote_specials(escape_latex(line)).replace(" ", "~")
|
||||
# An apostrophe is hidden as a marker: after a quoted caret it would
|
||||
# otherwise read back as "^" + "^'", the opening of a literal span, and
|
||||
# "@c \^' c@" -- the regex of that very katom -- failed the document
|
||||
# with a span that does not end. html_line hides it with the other
|
||||
# typographic characters; the marker decodes to the plain character.
|
||||
quoted = quote_specials(escape_latex(line)).replace("'", klammer_base.escape_ktesc("'"))
|
||||
return quoted.replace(" ", "~")
|
||||
|
||||
def widest_line(lines):
|
||||
r"""The line to measure the block's width with.
|
||||
@@ -325,12 +333,64 @@ class Code(klammer_base.Klammer_base):
|
||||
# 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:
|
||||
# A left-placed (or flowing) listing's caption is flush with it,
|
||||
# as in tex, where such a listing is unboxed and its caption a
|
||||
# paragraph on the same indent; a centred or right-placed one is
|
||||
# boxed there and its caption centred over the box, which is
|
||||
# the helper's default here too.
|
||||
align = "left" if self.hpos in ("left", "none") else None
|
||||
return html_util.add_caption(
|
||||
listing, "Listing", self.number, self.caption,
|
||||
self.caption_style, self.hpos, self.caption_side, True,
|
||||
self.caption_size, offset=self.offset)
|
||||
self.caption_size, offset=self.offset, caption_align=align)
|
||||
return html_util.hpos_container(listing, self.hpos, self.offset).str()
|
||||
|
||||
def txt_indent(self):
|
||||
# The block's indent in columns, by @table's rule: a left- or
|
||||
# right-placed block is inset by its :offset (0 = flush), ":hpos
|
||||
# none" flows with no indent, and "center" -- not attempted in plain
|
||||
# text -- keeps the block convention, Target_txt_indent.
|
||||
if self.hpos in ("left", "right"):
|
||||
return int(kutil.parse_width("txt", self.offset)[0])
|
||||
if self.hpos == "none":
|
||||
return 0
|
||||
return int(self.Target_txt_indent)
|
||||
|
||||
def txt(self):
|
||||
# The listing is LAID OUT here, line by line, and says so with the
|
||||
# verbatim marker (phases.txt_lay_out) so the justification pass
|
||||
# leaves it alone -- a blank line in the code included, which holds
|
||||
# a marker and therefore does not end the paragraph.
|
||||
#
|
||||
# Each line is prepared exactly as txt()'s inline sibling prepares a
|
||||
# fragment: the Klammertext specials QUOTED, because this result is
|
||||
# re-read as Klammertext (raw, a "#" removed the rest of its line and
|
||||
# a "code@" in a quoted source file closed the document's own span:
|
||||
# "A named end katom does not match"), and the typographic characters
|
||||
# hidden, so "--x" stays two hyphens. Spaces become the nbsp marker
|
||||
# AFTER that, not "~": hide_typographic has already hidden the
|
||||
# code's own tildes, and a "~" written here would be turned into a
|
||||
# space by the txt target's transform only as long as it survived.
|
||||
lines = self.text.strip("\n").split("\n")
|
||||
indent = txt_util.NBSP * self.txt_indent()
|
||||
def prepared(line):
|
||||
line = klammer_base.hide_typographic(quote_specials(line.rstrip()))
|
||||
return line.replace(" ", txt_util.NBSP)
|
||||
width = max(len(line.rstrip()) for line in lines)
|
||||
rule = indent + klammer_base.hide_typographic("-" * width)
|
||||
body = [indent + prepared(line) for line in lines]
|
||||
# A caption above the listing is ruled off by the caption pass, so
|
||||
# the listing draws its own top rule only when nothing does.
|
||||
captioned = self.number or self.caption
|
||||
caption_above = captioned and self.caption_side in ("top", "left")
|
||||
if not caption_above:
|
||||
body.insert(0, rule)
|
||||
body.append(rule)
|
||||
result = phases.txt_lay_out("\n".join(body))
|
||||
return txt_util.add_caption(
|
||||
result, "Listing", self.number, self.caption, self.caption_side,
|
||||
self.txt_indent())
|
||||
|
||||
|
||||
class Code_fragment(klammer_base.Klammer_base):
|
||||
def __init__(self, K):
|
||||
@@ -356,10 +416,13 @@ class Code_fragment(klammer_base.Klammer_base):
|
||||
# without a beginning) and the typographically active characters
|
||||
# hidden, so a "--" in code stays two hyphens.
|
||||
result = klammer_base.hide_typographic(quote_specials(self.code_text.strip()))
|
||||
# Surround code symbols with ‹ and ›, though there are conflicts in Swiss
|
||||
# German, Italian and some Scandinavian languages. Should be clear in
|
||||
# context, though.
|
||||
result = "\u2039" + result + "\u203A"
|
||||
|
||||
# If *nd* ("no delimiters) is false (the default), surround code symbols
|
||||
# with the characters ‹ and ›. There are conflicts in Swiss German,
|
||||
# Italian and some Scandinavian languages, but the meaning should be
|
||||
# clear in context.
|
||||
if not self.nd:
|
||||
result = "\u2039" + result + "\u203A"
|
||||
return result
|
||||
|
||||
def extract_marked_region(src, marker, filename):
|
||||
@@ -424,6 +487,9 @@ class Source(Code):
|
||||
# not exist until the file has been read. File content carries no
|
||||
# whitespace markers anyway, since those come from the katomizer.
|
||||
klammer_base.Klammer_base.__init__(self, K)
|
||||
# The filename arrives with the target's escapes as markers -- under
|
||||
# tex an underscore is one -- and a marker names no file.
|
||||
self.filename = klammer_base.unescape_ktesc(self.filename)
|
||||
try:
|
||||
with open(self.filename) as fp:
|
||||
text = fp.read()
|
||||
|
||||
@@ -30,7 +30,11 @@
|
||||
.code_block {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0 0 0 1rem;
|
||||
/* No left margin: it was 1rem, an html-only inset with no tex
|
||||
counterpart, which set the code a rem to the right of its own
|
||||
flush-left caption and of the same listing's pdf. A listing's
|
||||
inset is :offset, in both targets (the hpos_margin precedent). */
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,14 @@
|
||||
--serif: Libre Baskerville, serif;
|
||||
--sans-serif: Open Sans, sans-serif;
|
||||
--monospace: Inconsolata, monospace;
|
||||
/* The pitch of a line of body text, in em of the NOMINAL font size.
|
||||
It is 1.5 rather than LaTeX's 1.2 because font-size-adjust (below)
|
||||
draws the type larger than its nominal size -- EB Garamond by a
|
||||
fifth -- and line-height is computed from the nominal one. It is
|
||||
the html counterpart of \baselineskip, and a klammer whose spacing
|
||||
is stated in lines of text multiplies THIS (@table :leading), so a
|
||||
table and the paragraphs around it cannot drift apart. */
|
||||
--text_line_height: 1.5;
|
||||
}
|
||||
|
||||
body {
|
||||
@@ -32,7 +40,7 @@ h1, h2, h3, h4, h5 {
|
||||
place); this file assigns only the family. */
|
||||
|
||||
p {
|
||||
line-height: 1.5;
|
||||
line-height: var(--text_line_height);
|
||||
}
|
||||
|
||||
.plain {
|
||||
|
||||
+1
-1
@@ -195,7 +195,7 @@ other structures. The ^:offset is used only if the ^:hpos value is "left" or
|
||||
:number.bool true
|
||||
:caption_side.side
|
||||
:caption_style.style_code i
|
||||
:caption_size.float .9
|
||||
:caption_size.float 1
|
||||
: Arguments that define a caption for a block element.
|
||||
@@
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
# the front and back matter of a book):
|
||||
|
||||
@@section_args.o :id :n.int -1 :
|
||||
Common arguments for section headings. If *n* is not the default, it is used
|
||||
as the section number.
|
||||
Common arguments for section headings. If *n* is not the default,
|
||||
it is used as the section number.
|
||||
@@
|
||||
|
||||
@@preface.k :id :name Preface : Preface text @@
|
||||
|
||||
@@ -58,8 +58,21 @@ td {
|
||||
--cell-border: 1px black solid;
|
||||
}
|
||||
|
||||
/* The space between a rule and the text on either side of it: a cell is
|
||||
padded on the side that CARRIES the rule, so a row with no rule is
|
||||
untouched, a partial :hline pads only the cells it crosses, and the
|
||||
table's outer rules are padded on the inner side alone. The amounts
|
||||
began as booktabs' defaults (0.4ex above a rule, 0.65ex below one) and
|
||||
were tuned by eye, 2026-09-20: half of the space below moved above, so
|
||||
0.725ex above and 0.325ex below, the total unchanged. The tex target's
|
||||
pair is in table.py (TEX_RULE_ABOVE/_BELOW) and differs on purpose -- a
|
||||
TeX row and an html row distribute their own height differently around
|
||||
the text. --rulesep is @table's :rulesep, a multiplier, set on the
|
||||
<table> by table.py. "ex" is the x-height as RENDERED
|
||||
(font-size-adjust), so the space tracks the visible type. */
|
||||
.Bt {
|
||||
border-top: var(--cell-border);
|
||||
padding-top: calc(var(--rulesep, 1) * 0.325ex);
|
||||
}
|
||||
|
||||
.Br {
|
||||
@@ -68,6 +81,7 @@ td {
|
||||
|
||||
.Bb {
|
||||
border-bottom: var(--cell-border);
|
||||
padding-bottom: calc(var(--rulesep, 1) * 0.725ex);
|
||||
}
|
||||
|
||||
.Bl {
|
||||
|
||||
+11
-1
@@ -213,7 +213,8 @@
|
||||
:calc.table_calc
|
||||
:format.table_format
|
||||
:decimal.decimal_mark
|
||||
:leading.float 1.3
|
||||
:leading.float 1.0
|
||||
:rulesep.float 1
|
||||
:colsep 4pt
|
||||
:
|
||||
A table of rows of cells. The cells of a row are separated by "|" and the
|
||||
@@ -226,6 +227,15 @@ 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.
|
||||
|
||||
^:leading is the height of a row in lines of the surrounding text, the same
|
||||
in html and pdf. ^:rulesep is the space between a horizontal line and the
|
||||
text on either side of it, as a multiple of the standard amount (slightly
|
||||
more below a line than above it); 0 sets the lines against the rows. The
|
||||
lines at the top and the bottom of the table are spaced on their inner side
|
||||
only. A table with vertical lines gets no such space in a pdf, where it
|
||||
would break them. Neither argument has a meaning in plain text, where a row
|
||||
is a line of characters and a horizontal line already has a line to itself.
|
||||
@@
|
||||
|
||||
@@table.html,tex,txt :: @eval table.Table(K) eval@ @@
|
||||
|
||||
+77
-29
@@ -575,23 +575,39 @@ class Table(klammer_base.Klammer_base):
|
||||
# would wrap it as a stray <p> inside the table.
|
||||
return E("colgroup").body(cols.strip(), newline=False).str(), layout, table_width
|
||||
|
||||
def html_style(self):
|
||||
# :leading and :colsep under html (2026-09-18; both were tex-only).
|
||||
# \arraystretch multiplies the row strut, which is \baselineskip,
|
||||
# 1.2 times the font size -- so :leading L is a 1.2 L em row, and
|
||||
# the html equivalent is that line-height on the table, inherited
|
||||
# by every cell. The cells' vertical padding went with it
|
||||
# (table.css): an html-only extra with no tex counterpart, which
|
||||
# kept a :leading 1 table 0.3rem looser per row than its pdf.
|
||||
# :colsep is a custom property the td rule reads for its horizontal
|
||||
# padding (padding does not inherit); the flush-edge classes still
|
||||
# zero the outer ones. Measured 2026-09-18: rows of :leading 1 and
|
||||
# :leading 2 in the ratio 1:2 in both targets.
|
||||
line_height = f"{1.2 * float(self.leading):.4g}"
|
||||
def html_style(self, layout=""):
|
||||
# The WHOLE style attribute of the <table>, written once: "layout" is
|
||||
# the width declarations the caller has worked out (max-width for a
|
||||
# fill table, table-layout/width for fractions). E.attr() appends,
|
||||
# so a second .attr("style", ...) made a second attribute, which a
|
||||
# browser discards -- from 2026-09-18, when this function arrived,
|
||||
# to 2026-09-20 every width declaration was lost that way.
|
||||
# :leading and :colsep under html. ":leading L" is L times the pitch
|
||||
# of a line of the surrounding TEXT, in every target. tex has that
|
||||
# by construction: \arraystretch multiplies the row strut, which is
|
||||
# the \baselineskip in force. The html counterpart of \baselineskip
|
||||
# is --text_line_height (font.css, 1.5), so the table's line-height
|
||||
# is L times that, inherited by every cell. It was 1.2 L from
|
||||
# 2026-09-18 to 09-20, LaTeX's ratio of \baselineskip to font size:
|
||||
# wrong in html, where font-size-adjust draws the type larger than
|
||||
# the nominal size line-height is computed from, so a :leading 1
|
||||
# table sat 16% tighter than the paragraphs around it (measured:
|
||||
# 20.2px rows beside 24px text, where the pdf had 14.2pt beside
|
||||
# 14.2pt). The fallback serves a fragment without the stylesheet.
|
||||
# The cells have no vertical padding (table.css): an html-only extra
|
||||
# with no tex counterpart. :colsep is a custom property the td rule
|
||||
# reads for its horizontal padding (padding does not inherit); the
|
||||
# flush-edge classes still zero the outer ones.
|
||||
line_height = f"calc({float(self.leading):.4g} * var(--text_line_height, 1.5))"
|
||||
# Hidden: the result is re-read and the html target's transforms
|
||||
# would turn the property's "--" into an en dash (font.py's trap).
|
||||
return klammer_base.hide_typographic(
|
||||
f"line-height: {line_height}; --colsep: {self.colsep}")
|
||||
# :rulesep is a custom property too: the .Bt/.Bb rules (table.css)
|
||||
# read it for the padding on the side of a cell that carries a rule.
|
||||
style = (f"line-height: {line_height}; --colsep: {self.colsep}; "
|
||||
f"--rulesep: {float(self.rulesep):.4g}")
|
||||
if layout:
|
||||
style += "; " + layout.strip().rstrip(";")
|
||||
return klammer_base.hide_typographic(style)
|
||||
|
||||
def html(self):
|
||||
result = ""
|
||||
@@ -610,8 +626,10 @@ class Table(klammer_base.Klammer_base):
|
||||
# algorithm), matching the tex \ratio division. The max-width
|
||||
# cap goes on the caption wrapper when there is one (the table's
|
||||
# own percentage would be circular in a shrink-to-fit wrapper).
|
||||
result = E("table").body(result).attr("style", self.html_style())
|
||||
if self.number or self.caption:
|
||||
captioned = self.number or self.caption
|
||||
result = E("table").body(result).attr(
|
||||
"style", self.html_style("" if captioned else "max-width: 100%"))
|
||||
if captioned:
|
||||
result = html_util.add_caption(
|
||||
result, "Table", self.number, self.caption,
|
||||
self.caption_style, hpos=self.hpos,
|
||||
@@ -619,17 +637,19 @@ class Table(klammer_base.Klammer_base):
|
||||
font_size=self.caption_size, max_width="100%",
|
||||
offset=self.offset)
|
||||
else:
|
||||
result.attr("style", "max-width: 100%")
|
||||
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).attr("style", self.html_style())
|
||||
if self.number or self.caption:
|
||||
captioned = self.number or self.caption
|
||||
table_layout = ""
|
||||
if table_width:
|
||||
table_layout = f"{layout}width: " + ("100%" if captioned else table_width)
|
||||
result = E("table").body(colgroup + result).attr(
|
||||
"style", self.html_style(table_layout))
|
||||
if captioned:
|
||||
# The caption wrapper carries the width (a percentage on the
|
||||
# shrink-to-fit wrapper itself would be circular) and the table
|
||||
# fills it -- which also makes the caption track the table.
|
||||
if table_width:
|
||||
result.attr("style", f"{layout}width: 100%")
|
||||
result = html_util.add_caption(
|
||||
result, "Table", self.number, self.caption, self.caption_style,
|
||||
hpos=self.hpos, side=self.caption_side,
|
||||
@@ -638,8 +658,6 @@ class Table(klammer_base.Klammer_base):
|
||||
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, self.offset).str()
|
||||
return result
|
||||
|
||||
@@ -761,9 +779,32 @@ class Table(klammer_base.Klammer_base):
|
||||
# print("tex_column_spec:", "".join(parts))
|
||||
return "".join(parts)
|
||||
|
||||
TEX_RULE_ABOVE = 0.4 # ex; booktabs' \aboverulesep
|
||||
TEX_RULE_BELOW = 0.35 # 468 # ex; booktabs' \belowrulesep (0.65) less 20%, less 10%
|
||||
|
||||
def tex_rule_separations(self, top, bottom):
|
||||
# The space above and below a rule, as booktabs' \specialrule and
|
||||
# \cmidrule take them: :rulesep times TEX_RULE_ABOVE/_BELOW. They
|
||||
# began as booktabs' defaults (0.4ex above a rule, 0.65ex below) and
|
||||
# were tuned by eye, per target, 2026-09-20 (Andy): the amounts that
|
||||
# LOOK alike differ, because a TeX row carries its strut's depth
|
||||
# under the text and an html row its half-leading. The html pair is
|
||||
# in table.css (.Bt/.Bb). The table's outer rules are padded on the
|
||||
# INNER side only.
|
||||
rulesep = float(self.rulesep)
|
||||
above = "0pt" if top else f"{self.TEX_RULE_ABOVE * rulesep:.4g}ex"
|
||||
below = "0pt" if bottom else f"{self.TEX_RULE_BELOW * rulesep:.4g}ex"
|
||||
return above, below
|
||||
|
||||
def tex_hline(self, index):
|
||||
# Contiguous cell borders coalesce into single \cline runs; a
|
||||
# full-width line becomes \hline.
|
||||
# Contiguous cell borders coalesce into single rule runs. The rules
|
||||
# are booktabs' -- an \hline sits flush against the strut of the
|
||||
# rows on either side, and the only way to open space there is a
|
||||
# strut in every neighbouring row -- EXCEPT in a table with vertical
|
||||
# lines: a booktabs rule is set between rows, so it breaks every
|
||||
# vertical line it crosses (its author provides for none), and such
|
||||
# a table keeps \hline/\cline and gets no space at its rules.
|
||||
top = index == 0
|
||||
bottom = index == self.row_count
|
||||
if bottom:
|
||||
index -= 1
|
||||
@@ -773,7 +814,11 @@ class Table(klammer_base.Klammer_base):
|
||||
# No line through the interior of a merged (rowspan) cell.
|
||||
flags = [flag and (index, col_i) not in self.rowspan_covered
|
||||
for col_i, flag in enumerate(flags)]
|
||||
spaced = not self.s_vline.by_index and float(self.rulesep) > 0
|
||||
above, below = self.tex_rule_separations(top, bottom)
|
||||
if flags and all(flags):
|
||||
if spaced:
|
||||
return f"\\specialrule{{\\arrayrulewidth}}{{{above}}}{{{below}}}\n"
|
||||
return "\\hline\n"
|
||||
hline = ""
|
||||
start = None
|
||||
@@ -781,7 +826,10 @@ class Table(klammer_base.Klammer_base):
|
||||
if flag and start is None:
|
||||
start = i
|
||||
elif not flag and start is not None:
|
||||
hline += f"\\cline{{{start + 1}-{i}}} "
|
||||
if spaced:
|
||||
hline += f"\\cmidrule[\\arrayrulewidth]{{{start + 1}-{i}}} "
|
||||
else:
|
||||
hline += f"\\cline{{{start + 1}-{i}}} "
|
||||
start = None
|
||||
return hline.strip() + "\n"
|
||||
|
||||
@@ -1063,7 +1111,7 @@ class Table(klammer_base.Klammer_base):
|
||||
if len(rows) in self.s_hline.by_index:
|
||||
result += rule
|
||||
result = txt_util.add_caption(
|
||||
result, "Table", self.number, self.caption, self.caption_side, \
|
||||
int(self.Target_txt_indent))
|
||||
result, "Table", self.number, self.caption, self.caption_side,
|
||||
len(indent) // len(txt_util.NBSP))
|
||||
return result
|
||||
|
||||
@@ -103,6 +103,18 @@
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* A caption flush with its element's left edge (html_util.add_caption,
|
||||
caption_align "left"): @code's caption when the listing is placed left,
|
||||
as the tex target sets it. AFTER the two rules above, which it must
|
||||
override at equal specificity. */
|
||||
.caption_flush_left {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.caption_flush_left .caption {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.caption_left {
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
|
||||
@@ -238,7 +238,12 @@ def hpos_container(element, hpos, offset=None):
|
||||
|
||||
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, offset=None):
|
||||
font_size=.9, width=None, max_width=None, offset=None,
|
||||
caption_align=None):
|
||||
# caption_align "left" sets the caption flush with the element's left
|
||||
# edge, where the default centres it over the element: a caption above
|
||||
# a left-placed listing, which the tex target sets as a paragraph on the
|
||||
# listing's own indent.
|
||||
tag = element_tag(element)
|
||||
# Caption
|
||||
caption = ""
|
||||
@@ -276,6 +281,8 @@ def add_caption(element, caption_label, number, caption_text,
|
||||
#print("-"*80)
|
||||
|
||||
element = E("div").cls("caption_" + side).attr("data-label", caption_label).body(element)
|
||||
if caption_align == "left" and side in {"top", "bottom"}:
|
||||
element.cls("caption_flush_left")
|
||||
if width:
|
||||
# The element's width (e.g. a table's :column_width total) lives
|
||||
# on this wrapper: the element fills it, and the caption tracks
|
||||
|
||||
+69
-20
@@ -147,6 +147,36 @@ def tex_nl_marker():
|
||||
def tex_resolve_markers(text, K):
|
||||
return text.replace(tex_nl_marker(), "\\newline")
|
||||
|
||||
def txt_runs(par):
|
||||
"""A paragraph as runs of lines: [(laid_out, text), ...].
|
||||
|
||||
"Laid out or prose" was decided ONCE per paragraph, by its first line,
|
||||
so a laid-out block written directly after prose -- no blank line
|
||||
between them -- was re-flowed with it, every one of its lines marked
|
||||
or not: a generator whose prose ran straight into its @table put the
|
||||
table's header on the last line of the sentence (2026-09-20). A blank
|
||||
line should not be what protects a block that has already said, line
|
||||
by line, that it is laid out.
|
||||
|
||||
So a line that BEGINS WITH THE VERBATIM MARKER is laid out wherever it
|
||||
stands, and the paragraph divides into runs at each change. Only the
|
||||
marker divides: it is explicit and per line (txt_lay_out). A leading
|
||||
space or nbsp keeps the older, whole-paragraph meaning and only on the
|
||||
FIRST line, since a later line of prose may legitimately begin with
|
||||
one (a footnote's hanging indent).
|
||||
"""
|
||||
if par.startswith((" ", txt_nbsp_marker())):
|
||||
return [(True, par)]
|
||||
mark = txt_verbatim_marker()
|
||||
runs = []
|
||||
for line in par.split("\n"):
|
||||
laid_out = line.startswith(mark)
|
||||
if runs and runs[-1][0] == laid_out:
|
||||
runs[-1][1].append(line)
|
||||
else:
|
||||
runs.append((laid_out, [line]))
|
||||
return [(laid_out, "\n".join(lines)) for laid_out, lines in runs]
|
||||
|
||||
def txt_justify_blocks(text, K):
|
||||
delim = '__DIVIDE__'
|
||||
text = re.sub("\n\n+", delim, text)
|
||||
@@ -178,13 +208,20 @@ def txt_justify_blocks(text, K):
|
||||
# verbatim paragraph was not left alone, it was DELETED: the one
|
||||
# branch that had nothing to do also did not copy the text through.
|
||||
# An indented paragraph vanished from the output with no error.
|
||||
#
|
||||
# The decision is per RUN of lines, not per paragraph (txt_runs):
|
||||
# prose is filled, a laid-out run is copied through, and the runs
|
||||
# stay on consecutive lines as they were written.
|
||||
nbsp = txt_nbsp_marker()
|
||||
if par and not txt_is_laid_out(par):
|
||||
label = re.match("(?:" + re.escape(nbsp) + r")*\[\d+\] ", par)
|
||||
par = "\n".join(txt_wrap(
|
||||
par, int(K.Target_txt_width),
|
||||
subsequent_indent=" " * len(label.group(0)) if label else ""))
|
||||
result += par + "\n\n"
|
||||
filled = []
|
||||
for laid_out, run in txt_runs(par):
|
||||
if run and not laid_out:
|
||||
label = re.match("(?:" + re.escape(nbsp) + r")*\[\d+\] ", run)
|
||||
run = "\n".join(txt_wrap(
|
||||
run, int(K.Target_txt_width),
|
||||
subsequent_indent=" " * len(label.group(0)) if label else ""))
|
||||
filled.append(run)
|
||||
result += "\n".join(filled) + "\n\n"
|
||||
result = re.sub(" *" + txt_vspace_marker() + " *", "\n", result)
|
||||
result = re.sub(r"\n? *" + txt_nl_marker() + r" *\n?", "\n", result)
|
||||
result = result.replace(txt_verbatim_marker(), "")
|
||||
@@ -192,33 +229,45 @@ def txt_justify_blocks(text, K):
|
||||
return result
|
||||
|
||||
def txt_insert_captions(text, K):
|
||||
# The caption's INDENT is the block's, read off the run of nbsp markers
|
||||
# written before the caption marker (txt_util.add_caption) -- not
|
||||
# Target_txt_indent, which put the caption of a flush block two columns
|
||||
# to the right of the block it captions. Every line produced here is
|
||||
# marked laid-out: a flush caption begins with neither a space nor an
|
||||
# nbsp, and an unmarked first line made the justification pass re-flow
|
||||
# the caption, its rule and the whole block under it into one paragraph.
|
||||
mark = kutil.caption_delimiter()
|
||||
caption_rgx = re.compile(f"{mark}(.*?){mark}(.*?){mark}", re.S)
|
||||
caption_number = 0
|
||||
nbsp = txt_nbsp_marker()
|
||||
caption_rgx = re.compile(
|
||||
f"((?:{re.escape(nbsp)})*){mark}(.*?){mark}(.*?){mark}", re.S)
|
||||
# One sequence PER LABEL, as in html and tex: a listing after a table is
|
||||
# "Listing 1", not "Listing 2". It was one counter for every label.
|
||||
caption_numbers = {}
|
||||
width = int(K.Target_txt_width)
|
||||
indent = int(K.Target_txt_indent)
|
||||
indented_width = width - indent
|
||||
def replace(m):
|
||||
nonlocal caption_number
|
||||
name, text = m.groups()
|
||||
indent_run, name, text = m.groups()
|
||||
indent = len(indent_run) // len(nbsp)
|
||||
indented_width = max(1, width - indent)
|
||||
# An empty label means an unnumbered caption (":number false"). It
|
||||
# must NOT take a number from the sequence: a reader counting the
|
||||
# captions they can see would then find the next one a number
|
||||
# ahead. The same is true of html and tex, where an unnumbered
|
||||
# caption emits no marker and so never reaches their counter.
|
||||
if name:
|
||||
caption_number += 1
|
||||
caption = f"{name} {caption_number} - {text}"
|
||||
caption_numbers[name] = caption_numbers.get(name, 0) + 1
|
||||
caption = f"{name} {caption_numbers[name]} - {text}"
|
||||
else:
|
||||
caption = text
|
||||
# Visible widths throughout: a caption may carry style markers.
|
||||
lines = [caption]
|
||||
if txt_visible_length(caption) > indented_width:
|
||||
caption = "\n".join(txt_wrap(
|
||||
caption, indented_width,
|
||||
subsequent_indent=txt_nbsp_marker()*indent))
|
||||
line_length = max([txt_visible_length(e) for e in caption.split("\n")])
|
||||
line = txt_nbsp_marker() * indent + "-" * line_length
|
||||
return caption + "\n" + line
|
||||
lines = txt_wrap(caption, indented_width)
|
||||
line_length = max(txt_visible_length(e) for e in lines)
|
||||
lines.append("-" * line_length)
|
||||
lead = txt_verbatim_marker() + indent_run
|
||||
# The first line's marker is already in the text, before the indent
|
||||
# run this match begins with (add_caption); the others get theirs.
|
||||
return indent_run + ("\n" + lead).join(lines)
|
||||
result = caption_rgx.sub(replace, text)
|
||||
return result
|
||||
|
||||
|
||||
@@ -101,7 +101,11 @@ def add_caption(element, caption_label, number, caption_text,
|
||||
caption = kutil.caption_marker("", caption_text, None)
|
||||
else:
|
||||
return element
|
||||
caption = indent_text(re.sub(r"\s*\n\s*", " ", caption.strip()), indent)
|
||||
# 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
|
||||
Reference in new issue
Block a user