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)
This commit is contained in:
233
sks/tns/markdown.css
Normal file
233
sks/tns/markdown.css
Normal file
@@ -0,0 +1,233 @@
|
||||
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);
|
||||
}
|
||||
|
||||
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 */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user