Files
klammertext/sks/tns/markdown.css

245 lines
8.9 KiB
CSS
Raw Permalink Normal View History

Markdown to PDF: the mdpdf command, and the fonts it needs This snapshot carries sks/tns/, the translation directory, into the distribution for the first time, together with the two default font families its stylesheet names. sks/tns/ holds two converters in opposite directions. md_to_sks.py converts Markdown to Klammertext, recording what it cannot convert exactly as "#[MD ... ]#" markers so a draft carries its own worklist. md_to_pdf.py renders Markdown straight to PDF through a headless Chromium driven over the DevTools Protocol, bypassing Klammertext entirely -- the route for a document that is not ready to convert, and a permanent one for Markdown that Klammertext cannot represent well. Neither is loaded by the SKS; md_to_pdf needs markdown-it-py, which it keeps in a virtual environment of its own and creates with --setup. The everyday form of the second is the mdpdf command, a shell function in sks/tns/mdpdf.sh that env/runtime.env sources, so anyone with the Klammertext environment has it: mdpdf notes.md # writes notes.pdf beside it It supplies the house fonts, the size matching, and the code wrapping, completes on *.md at the TAB key, and takes its defaults from MDPDF_* variables so one can be changed in a shell profile without copying the function. It is POSIX shell rather than zsh, since runtime.env is sourced from bash profiles too. Two things the stylesheet does that a print stylesheet usually cannot. Code lines are wrapped to a column count MEASURED from the rendered page rather than written down -- the browser is asked how many characters a code box holds, over every box in the document, so the wrapping stays right when the fonts, sizes or margins change. And the page number is a CSS Paged Media margin box, which current Chromium implements, so it is set in the document's own face instead of the browser's generic sans. fnt/ gains EB Garamond and Source Sans 3, the serif and sans the stylesheet asks for by default. (from dev 97d4f244c737)
2026-08-09 15:54:05 +02:00
body {
font-family: var(--serif);
font-size: 100%;
line-height: 1.3;
}
/* The monospace font is scaled so its letters are the height of the serif
font's, rather than by eye: md_to_pdf.py computes --mono-scale from the
two fonts' x-height and cap-height (the same rule the SKS applies), so
this stays right if either font is changed. */
code, pre {
font-family: var(--mono);
font-size: calc(1em * var(--mono-scale, 1));
}
/* Once, not twice. A fenced block is <pre><code>, so both elements match
the rule above and the scale is applied to the scaled size: measured
before this rule, inline code came out at 14.00px and the same code in a
box at 12.26px -- a metric-matched scale silently defeating itself. The
code in a box inherits the pre's size instead. */
pre code {
font-size: 1em;
}
h1, h2, h3, h4, h5, h6 {
font-family: var(--sans);
font-weight: normal;
padding-top: .5rem;
padding-bottom: 0rem;
margin-top: .0rem;
margin-bottom: 0rem;
}
h1 {
font-size: 1.2rem;
}
h2 {
font-size: 1.1rem;
}
h3, h4, h5, h6 {
font-size: 1.0rem;
}
/* The gap between a section number and its title, in a heading and in a
table-of-contents entry alike. The number is plain text in the Markdown
("## 2.9 Keystone correction", "- [2.9 Keystone correction](#29-...)");
md_to_pdf.py wraps it in this span and eats the space that followed it, so
the whole distance is set here. Change --secnum-gap to widen or narrow
it, or give the contents its own gap with a "li .secnum" rule. To rule a
contents into columns rather than merely space it, add
"display: inline-block" and a "min-width" there: the titles then align
instead of each starting one gap after a number of its own width. */
:root {
--secnum-gap: 0.5em;
}
.secnum {
margin-right: var(--secnum-gap);
}
Render correctly under a sandboxed browser; unbulleted contents mdpdf drives a headless Chromium, and a browser installed as a flatpak -- which is what the Pop!_Shop installs, and so the ordinary case on a Pop!_OS or System76 machine -- was unusable in two ways, the second of them silent. It was not found at all. A flatpak puts nothing on PATH and nothing in /opt, and its wrapper is named com.brave.Browser rather than brave-browser, so adding the export directory to PATH would not have helped either. The application ids are now looked for in the flatpak export directories, after every native browser, so a native one still wins where there is one. Found, it then rendered in the wrong fonts and reported success. The @font-face URLs pointed into the font store, which the sandbox cannot read, and a browser does not report a font it cannot fetch -- it substitutes. The PDF came out in a default serif and nothing said so. Granting the path would not have travelled either: sandbox filesystem permissions differ from one application to the next, so a scheme resting on a path works with one browser and fails with another on the same machine. So the document, its fonts and its images are now served to the browser over the loopback interface instead of being passed as file:// paths. Every sandbox shares the network namespace -- the DevTools connection already depends on it -- so this needs no filesystem permission from any sandbox, present or future. A --keep-html copy is still written with file:// URLs, so it works when nothing is serving it. A font that fails to load is now an error rather than a substitution: the page is asked whether each requested family arrived, and no PDF is written if one did not. A finished-looking document in the wrong typeface is the worst failure this program can have. Separately, a table-of-contents entry no longer carries a bullet. An entry is a section title, and a marker in front of it reads as a list of things rather than as a contents; ordinary bulleted lists are unaffected. (from dev 12929fdff53b)
2026-08-09 20:24:20 +02:00
/* A table-of-contents entry carries no bullet. Each entry is a section
title, and a marker in front of it reads as a list of things rather than
as a contents. md_to_pdf.py puts this class on a list item whose first
content is a link to a fragment, which is what a Markdown contents is, so
an ordinary bulleted list keeps its bullets. The entries stay on the
list's indent; add "padding-left: 0" to the surrounding ul to bring a
contents out to the margin instead. */
li.toc {
list-style: none;
}
Markdown to PDF: the mdpdf command, and the fonts it needs This snapshot carries sks/tns/, the translation directory, into the distribution for the first time, together with the two default font families its stylesheet names. sks/tns/ holds two converters in opposite directions. md_to_sks.py converts Markdown to Klammertext, recording what it cannot convert exactly as "#[MD ... ]#" markers so a draft carries its own worklist. md_to_pdf.py renders Markdown straight to PDF through a headless Chromium driven over the DevTools Protocol, bypassing Klammertext entirely -- the route for a document that is not ready to convert, and a permanent one for Markdown that Klammertext cannot represent well. Neither is loaded by the SKS; md_to_pdf needs markdown-it-py, which it keeps in a virtual environment of its own and creates with --setup. The everyday form of the second is the mdpdf command, a shell function in sks/tns/mdpdf.sh that env/runtime.env sources, so anyone with the Klammertext environment has it: mdpdf notes.md # writes notes.pdf beside it It supplies the house fonts, the size matching, and the code wrapping, completes on *.md at the TAB key, and takes its defaults from MDPDF_* variables so one can be changed in a shell profile without copying the function. It is POSIX shell rather than zsh, since runtime.env is sourced from bash profiles too. Two things the stylesheet does that a print stylesheet usually cannot. Code lines are wrapped to a column count MEASURED from the rendered page rather than written down -- the browser is asked how many characters a code box holds, over every box in the document, so the wrapping stays right when the fonts, sizes or margins change. And the page number is a CSS Paged Media margin box, which current Chromium implements, so it is set in the document's own face instead of the browser's generic sans. fnt/ gains EB Garamond and Source Sans 3, the serif and sans the stylesheet asks for by default. (from dev 97d4f244c737)
2026-08-09 15:54:05 +02:00
p, ol, ul {
margin-top: 0.5rem;
margin-bottom: 0.5rem;
}
pre {
/* A code line wider than the page does not overflow or clip when
printing: Chromium shrinks the WHOLE document until it fits, silently,
by a factor that changes with the longest line. Wrapping guarantees
that never happens. md_to_pdf.py --wrap-code breaks shell lines at a
word boundary with a "\" first, which is copy-pasteable; this catches
what it cannot wrap (PowerShell, .ini, ASCII diagrams). */
white-space: pre-wrap;
overflow-wrap: anywhere;
padding-left: 0rem;
border: 1px solid #888;
background-color: #EFE;
}
th {
border-bottom: 1px solid #000;
}
table, th, td {
border: 1px solid gray;
}
@media print {
/* A section title never sits alone at the foot of a page: the break is
forbidden between a heading and whatever follows it, so the heading
moves to the next page with its text. break-inside keeps a heading
that wraps from being split across the fold.
Chromium honours these when it paginates for printing (measured on a
16-page test document: three headings stranded at a page bottom
before, none after, and the page count unchanged). A heading it
cannot honour them for -- one taller than the page -- is laid out
as if they were absent rather than looped over, which is the
specified behaviour for an unsatisfiable avoid.
This does NOT govern how much of the following paragraph comes with
the heading: that is `orphans`, below. */
h1, h2, h3, h4, h5, h6 {
break-after: avoid;
break-inside: avoid;
}
/* A paragraph split by a page break leaves at least two lines on each
side of it: never one line stranded at the foot of a page (orphans),
never one line arriving alone at the top of the next (widows) -- and
with break-after above, that top-of-page line would be the one
directly under a heading the break has just moved. Both properties
are inherited, so declaring them on body covers list items and table
cells too.
Measured on a 16-page test document: Chromium honours both -- with
orphans: 1 it packs the text into 15 pages, and with widows: 1 it
moves a line back across a page boundary (same page count, different
pagination) -- and 2 is ALREADY its default for both, so rendering
with these declarations is byte-identical to rendering without them.
They are here to pin the values, not to change today's output: the CSS
initial value is 2 but a UA is free to differ, and a stylesheet added
after this one could relax it. */
body {
orphans: 2;
widows: 2;
}
pre {
padding: .75rem 1.25rem .75rem 1.25rem;
break-inside: avoid;
}
a[href]:after {
content: none;
}
th, td {
padding: .25rem .75rem;
}
table {
margin-left: 1rem;
margin-bottom: .5rem;
border-collapse: collapse;
}
th {
border-bottom: 1px solid #000;
text-align: left;
padding-right: 2rem;
padding-bottom: 0.2em;
}
tbody tr:first-child td {
padding-top: 0.0em;
}
blockquote {
margin-left: 1rem;
}
table, th, td {
border: 1px solid #BBB;
}
:root {
--link-blue: #042B8C; /* #03216C; */
}
a {
color: var(--link-blue);
}
a, a:link, a:visited {
outline: 0;
text-decoration: none;
}
a:hover {
text-decoration: underline;
text-underline-offset: 0.2em;
text-decoration-thickness: 1px;
outline: 0;
}
/* The page number, in the document's own face.
This is a CSS Paged Media margin box, and it is the reason to prefer
it over md_to_pdf.py's --page-numbers: Chromium's footer template is
rendered in a context of its own that cannot see this stylesheet, so
the number comes out in a generic sans, while var(--serif) here is the
EB Garamond the text is set in. Measured 2026-08-09 on Brave 151:
margin boxes work and count pages. They are recent, though -- an
older Chromium silently prints no number at all.
DO NOT ALSO PASS --page-numbers. The two mechanisms are independent
and both would draw, one over the other, and the flag additionally
widens the bottom margin.
HEIGHT. A margin box is drawn in the margin and cannot be moved out
of it, so where the number sits and how much text a page holds are
one question, not two. Chromium centres the box in the margin band
by default; these declarations pin it to the top of a slightly larger
band instead, which puts the number 27pt above the paper edge rather
than 12 -- a body line higher -- while the padding, all 1.5pt of it,
leaves the text block ending just above it. The text therefore keeps
the line it would otherwise have lost to the raised number: measured,
a document that grew from 3 pages to 4 when the number was raised
alone is back to 3.
THE TIGHT SPOT, and the number to change if it shows. Text and
number now share a narrow band, so on a page whose last line reaches
the bottom of the block they nearly touch: measured worst pages over
three documents, 7.6 / 3.1 / 2.4pt between the last line's box and
the number's. Nothing overlapped, but 2.4pt is close. Giving the
text back only HALF a line -- margin-bottom 0.60in, padding-top
4.4pt, the number within a point of where it is now -- measured
8.4 / 9.1 / 11.4pt instead, and cost no document a page.
var(--serif) is safe here because the body is set in it, so the
webfont is loaded. A face the document does not otherwise use is NOT
loaded for a margin box: var(--mono) in a prose-only document fell
back to the platform monospace, and in one variant the box rendered
nothing whatever -- silently, as ever with print CSS. */
@page {
margin-bottom: 0.56in; /* the number, and little else */
@bottom-center {
content: counter(page) " of " counter(pages);
font-family: var(--serif);
font-size: 9pt;
color: black;
vertical-align: top; /* not centred in the margin band */
padding-top: 1.5pt; /* its distance from the text */
}
}
}