Simplify image/figure styling: markdown always renders standalone images as figures
Standalone markdown images (captioned or not) now become block <figure> elements; only inline-with-text images and raw author <img> HTML stay plain. This collapses the img/figure selector duplication in pagerite.css into a default + override structure: figure fills the column, floats go 30% (1em text gap), an explicit width attribute shrink-wraps the figure (images with width are left untouched by CSS so the attribute hint survives), and .wide re-anchors are grouped with the main rule.
This commit is contained in:
@@ -91,8 +91,10 @@ not for the public pages. See `docs/design-principles.md` for the design.
|
||||
`/favicon.ico` by convention.
|
||||
- `markdown.py` — markdown-it-py renderer (html passthrough + attrs,
|
||||
footnote, deflist, tasklists plugins; typographer + breaks on). Custom
|
||||
image rule: relative srcs resolve against the page path, titled images
|
||||
become figures. A `{dates}` line expands to the article's
|
||||
image rule: relative srcs resolve against the page path; an image
|
||||
standing alone in its paragraph becomes a figure (captioned when
|
||||
titled), while inline-with-text images and raw <img> HTML stay plain.
|
||||
A `{dates}` line expands to the article's
|
||||
published/updated dateline (`p.dateline`, from `Node.created`/
|
||||
`modified`; left literal in previews of unsaved pages).
|
||||
- `views.py` — the shared page layout as an html5tagger `Template` with
|
||||
|
||||
@@ -69,11 +69,13 @@ evolves.
|
||||
are stored by content hash — blake3, first 6 bytes hex + original
|
||||
extension — and served immutable from `/_f/{hash}.ext`. Absolute URLs
|
||||
that survive page renames and dedupe identical content; pages no longer
|
||||
own files. An image with a title becomes a `<figure>` with
|
||||
`<figcaption>`. Positioning is by attribute classes:
|
||||
own files. An image standing alone in its paragraph becomes a block
|
||||
`<figure>` — with `<figcaption>` when it has a title; images inline
|
||||
with text and raw `<img>` HTML stay plain inline images. Positioning
|
||||
is by attribute classes:
|
||||
`{.right}` — `{.right}`, `{.left}` float at
|
||||
30% of the text column (the caption wraps within it; an explicit
|
||||
`width=300` overrides on uncaptioned images),
|
||||
`width=300` makes the figure shrink-wrap the image instead),
|
||||
`{.wide}` goes full bleed (viewport edge to edge, or up to the docked
|
||||
editor; the sidebar stacks on top of it); plain attributes like `width=300`
|
||||
work too. Headings (h1/h2) clear floats, so images never overflow into the
|
||||
|
||||
@@ -609,40 +609,57 @@ td {
|
||||
padding: 0.35rem 0.8rem;
|
||||
}
|
||||
|
||||
/* Images and figures */
|
||||
/* Images and figures
|
||||
|
||||
Markdown images standing alone in a paragraph render as a block
|
||||
<figure> (with <figcaption> when the image has a title); the
|
||||
brace-attribute positioning class ({.left}, {.right}, {.wide}) lives
|
||||
on the img inside, but only the figure is ever positioned, so the
|
||||
caption stays below the image. Raw <img> HTML written by the author
|
||||
stays inline and unstyled beyond these defaults. */
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
/* Figures are block containers filling the column; the image fills the
|
||||
figure. The auto inline margins only take effect once a rule below
|
||||
shrinks the width (centering the figure); at 100% they compute to 0. */
|
||||
figure {
|
||||
margin: 0 0 1.5rem;
|
||||
margin: 0 auto 1.5rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
figcaption {
|
||||
color: var(--muted);
|
||||
font-size: 0.85rem;
|
||||
/* The image fills the figure, except when it carries an explicit width
|
||||
attribute — that width is a browser presentational hint which CSS
|
||||
cannot restore once overridden, so such images are simply left alone. */
|
||||
figure img:not([width]) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Captions of full-bleed images: centered and kept to a readable width. */
|
||||
figure:has(.wide) figcaption {
|
||||
max-width: 65ch;
|
||||
margin-inline: auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Positioning via brace-attribute classes: {.right}, {.left}, {.wide} */
|
||||
figure:has(.right),
|
||||
img.right {
|
||||
/* Floated figures: {.right} / {.left}, defaulting to 30% of the column
|
||||
and capped at half of it. */
|
||||
figure:has(.right) {
|
||||
float: right;
|
||||
margin: 0.3rem 0 1rem 1.5rem;
|
||||
width: 30%;
|
||||
max-width: 50%;
|
||||
margin: 0.3rem 0 1rem 1em;
|
||||
}
|
||||
|
||||
figure:has(.left),
|
||||
img.left {
|
||||
figure:has(.left) {
|
||||
float: left;
|
||||
margin: 0.3rem 1.5rem 1rem 0;
|
||||
width: 30%;
|
||||
max-width: 50%;
|
||||
margin: 0.3rem 1em 1rem 0;
|
||||
}
|
||||
|
||||
/* An image with an explicit width attribute shrink-wraps instead: the
|
||||
figure fits the image and, per the auto inline margins above, centers
|
||||
in the column. Placed after the percentage widths above so it
|
||||
overrides them at equal specificity. */
|
||||
figure:has(img[width]) {
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
/* .wide is full bleed: edge to edge of the viewport (or of the space right
|
||||
@@ -651,52 +668,81 @@ img.left {
|
||||
symmetric margins of (50% of the column − 50% of the target width) break
|
||||
out of the column and center the element on the same center — making the
|
||||
left edge land exactly at the viewport's edge (or the editor's right
|
||||
border). The sidebar stacks above it (z-index + translucent blur). */
|
||||
figure:has(.wide),
|
||||
img.wide {
|
||||
display: block;
|
||||
/* no inline strut/descender gaps around the image */
|
||||
border). The sidebar stacks above it (z-index + translucent blur).
|
||||
|
||||
The rules below re-anchor the bleed for the layouts where the article
|
||||
is not viewport-centered; each just overrides width/margin-inline, and
|
||||
later rules win at equal specificity. */
|
||||
figure:has(.wide) {
|
||||
width: 100vw;
|
||||
max-width: none;
|
||||
margin-inline: calc(50% - 50vw);
|
||||
/* negative margins also kill the UA figure margin */
|
||||
/* Follow the docked editor's margin-left transition smoothly. */
|
||||
transition: margin-left 0.25s ease;
|
||||
}
|
||||
|
||||
/* A paragraph wrapping only a wide image must not add its line height. */
|
||||
p:has(> img.wide:only-child) {
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
/* A captioned image is a <figure>, and the positioning class lives on the
|
||||
img inside it: only the figure may float/shift, never the img itself,
|
||||
or the caption would wrap beside the image instead of sitting below.
|
||||
The image fills the figure's width. */
|
||||
figure:has(.right) img,
|
||||
figure:has(.left) img {
|
||||
float: none;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Full bleed means edge to edge — no rounded corners. */
|
||||
figure:has(.wide) img {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
margin: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
/* Floats take a fixed share of the text column rather than sizing by the
|
||||
image's intrinsic width, which varies wildly (SVGs have none, and
|
||||
min-content collapses them). A percentage also scales correctly inside
|
||||
multi-column segments, where the column is the containing block. The
|
||||
caption wraps within that width. A width attribute ({.right width=300})
|
||||
overrides the default on uncaptioned images. */
|
||||
figure:has(.right),
|
||||
figure:has(.left),
|
||||
img.right:not([width]),
|
||||
img.left:not([width]) {
|
||||
width: 30%;
|
||||
/* .wide on multicol pages: the article is not viewport-centered (no right
|
||||
gutter), so the bleed anchors at the left gutter — the 1fr share of the
|
||||
1fr + 4fr grid, i.e. 20vw — plus main's padding, and spans on to the
|
||||
right viewport edge. */
|
||||
body:has(.multicol) figure:has(.wide) {
|
||||
margin-inline: calc(-20vw - 1.25rem) 0;
|
||||
}
|
||||
|
||||
/* Editing: shrink the bleed to the space right of the docked editor. */
|
||||
body.editing figure:has(.wide) {
|
||||
width: calc(100vw - var(--editor-w));
|
||||
margin-inline: calc(50% - (100vw - var(--editor-w)) / 2);
|
||||
}
|
||||
|
||||
/* Editing + multicol: the left gutter is 1/5 of the space right of the
|
||||
editor, and the bleed also crosses #content's 1rem editing gap plus
|
||||
main's 1.25rem left padding (the gutter shrink from the padding roughly
|
||||
cancels the rounding): −2rem in total. */
|
||||
body.editing:has(.multicol) figure:has(.wide) {
|
||||
margin-inline: calc((100vw - var(--editor-w)) / -5 - 2rem) 0;
|
||||
}
|
||||
|
||||
/* Narrow windows with a sidebar: below 102rem the symmetric gutters can no
|
||||
longer both hold the 12rem sidebar, so #content reserves it with a fixed
|
||||
left track (see the matching media query below) and the article always
|
||||
starts at 12rem (+ main's 1.25rem padding) — the bleed margin is a plain
|
||||
constant. Scoped by :has(#sidebar) since the sidebar element is omitted
|
||||
entirely on pages without sub-navigation, and excluded while editing,
|
||||
where the editing rules above apply instead. */
|
||||
@media (max-width: 102rem) {
|
||||
body:has(#sidebar):not(.editing) figure:has(.wide) {
|
||||
margin-inline: -13.25rem 0;
|
||||
}
|
||||
}
|
||||
|
||||
figcaption {
|
||||
color: var(--muted);
|
||||
line-height: 1;
|
||||
font-size: 0.85rem;
|
||||
hyphens: auto;
|
||||
-webkit-hyphens: auto;
|
||||
text-wrap: pretty;
|
||||
text-align: left;
|
||||
/* Never let a long caption stretch a shrink-to-fit figure wider than the
|
||||
image; the caption wraps at the figure's width instead. */
|
||||
width: 0;
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
/* Full-bleed figures: center the caption, shrink-wrapped to its text and
|
||||
capped at a readable width. The min-width shrink-wrap trick above would
|
||||
win over max-width (min beats max per spec), so it is reset here. */
|
||||
figure:has(.wide) figcaption {
|
||||
width: fit-content;
|
||||
min-width: 0;
|
||||
max-width: 65ch;
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
/* Section headings start below any floated figures of the previous
|
||||
@@ -709,9 +755,10 @@ article h2 {
|
||||
/* Narrow windows with a sidebar: below 102rem the symmetric gutters can no
|
||||
longer both hold the 12rem sidebar, so reserve its space with a fixed
|
||||
left track instead of letting it overlap the article. The article then
|
||||
always starts at 12rem (+ main's 1.25rem padding), so the .wide breakout
|
||||
margin is a plain constant. Scoped by :has(#sidebar) since the sidebar
|
||||
element is omitted entirely on pages without sub-navigation. */
|
||||
always starts at 12rem (+ main's 1.25rem padding), which the matching
|
||||
.wide breakout rule in the images section relies on. Scoped by
|
||||
:has(#sidebar) since the sidebar element is omitted entirely on pages
|
||||
without sub-navigation. */
|
||||
@media (max-width: 102rem) {
|
||||
body:has(#sidebar):not(.editing) #content {
|
||||
grid-template-columns: 12rem minmax(0, 78rem) minmax(0, 1fr);
|
||||
@@ -720,40 +767,10 @@ article h2 {
|
||||
/* Long articles stay fluid here too: same 12rem reservation for the
|
||||
sidebar, then the article takes everything to the right viewport
|
||||
edge. The article's left edge stays at 12rem either way, so the
|
||||
constant .wide breakout margin below remains correct. */
|
||||
constant .wide breakout margin remains correct. */
|
||||
body:has(#sidebar):has(.multicol):not(.editing) #content {
|
||||
grid-template-columns: 12rem minmax(0, 1fr);
|
||||
}
|
||||
|
||||
body:has(#sidebar) figure:has(.wide),
|
||||
body:has(#sidebar) img.wide:not(figure img) {
|
||||
margin-inline: -13.25rem 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* .wide on multicol pages: the article is not viewport-centered (no right
|
||||
gutter), so the bleed anchors at the left gutter — the 1fr share of the
|
||||
1fr + 4fr grid, i.e. 20vw — plus main's padding, and spans on to the
|
||||
right viewport edge. Loses to the sidebar rule above (id in :has) and to
|
||||
the editing rules below (same specificity, later in the file). */
|
||||
body:has(.multicol) figure:has(.wide),
|
||||
body:has(.multicol) img.wide:not(figure img) {
|
||||
margin-inline: calc(-20vw - 1.25rem) 0;
|
||||
}
|
||||
|
||||
/* Editing: shrink the bleed to the space right of the docked editor. */
|
||||
body.editing figure:has(.wide),
|
||||
body.editing img.wide:not(figure img) {
|
||||
width: calc(100vw - var(--editor-w));
|
||||
margin-inline: calc(50% - (100vw - var(--editor-w)) / 2);
|
||||
}
|
||||
|
||||
/* Editing + multicol: the left gutter is 1/5 of the space right of the
|
||||
editor. */
|
||||
body.editing:has(.multicol) figure:has(.wide),
|
||||
body.editing:has(.multicol) img.wide:not(figure img) {
|
||||
width: calc(100vw - var(--editor-w));
|
||||
margin-inline: calc((100vw - var(--editor-w)) / -5 - 1.25rem) 0;
|
||||
}
|
||||
|
||||
/* Scroll reveal (pagerite.js adds .reveal/.in; JS off = fully visible) */
|
||||
|
||||
+15
-7
@@ -14,8 +14,10 @@ spans/blocks and raw HTML are left untouched.
|
||||
|
||||
Images get special treatment: a relative `src` is resolved against the
|
||||
page's own path (so `` in `/docs/design` is served from
|
||||
`/docs/design/photo.avif`), and an image with a title becomes a
|
||||
`<figure>` with `<figcaption>`. Positioning is done with attribute
|
||||
`/docs/design/photo.avif`), and an image standing alone in its paragraph
|
||||
becomes a block `<figure>` — with `<figcaption>` when it has a title.
|
||||
Images inline with other content stay plain inline `<img>`, as does raw
|
||||
`<img>` HTML written by the author. Positioning is done with attribute
|
||||
classes, e.g. `{.right}`.
|
||||
"""
|
||||
|
||||
@@ -70,16 +72,22 @@ def _image_rule(
|
||||
token.attrs["src"] = f"/{page}/{src}" if page else f"/{src}"
|
||||
token.attrs["alt"] = self.renderInlineAsText(token.children, options, env)
|
||||
img = self.renderToken(tokens, idx, options, env)
|
||||
if title := token.attrs.get("title"):
|
||||
return f"<figure>{img}<figcaption>{escapeHtml(title)}</figcaption></figure>"
|
||||
if len(tokens) == 1:
|
||||
# The only inline content of its paragraph: render as a block
|
||||
# figure, captioned when titled. (The <p> wrapper is dropped by
|
||||
# _unwrap_lone_figures below.)
|
||||
title = token.attrs.get("title")
|
||||
caption = f"<figcaption>{escapeHtml(title)}</figcaption>" if title else ""
|
||||
return f"<figure>{img}{caption}</figure>"
|
||||
# Inline with other content: a plain inline image.
|
||||
return img
|
||||
|
||||
|
||||
def _unwrap_lone_figures(state) -> None:
|
||||
"""Drop the <p> wrapper around a lone titled image.
|
||||
"""Drop the <p> wrapper around a lone image.
|
||||
|
||||
markdown-it wraps inline content in a paragraph, but our image rule
|
||||
turns titled images into <figure> — a block element that is invalid
|
||||
turns lone images into <figure> — a block element that is invalid
|
||||
inside <p>. Browsers hoist it out, leaving an empty paragraph whose
|
||||
margins disturb the layout.
|
||||
"""
|
||||
@@ -88,7 +96,7 @@ def _unwrap_lone_figures(state) -> None:
|
||||
if token.type != "inline" or not token.children:
|
||||
continue
|
||||
[child] = token.children if len(token.children) == 1 else [None]
|
||||
if child and child.type == "image" and child.attrs.get("title"):
|
||||
if child and child.type == "image":
|
||||
if (tokens[i - 1].type == "paragraph_open"
|
||||
and tokens[i + 1].type == "paragraph_close"):
|
||||
tokens[i - 1].hidden = True
|
||||
|
||||
Reference in New Issue
Block a user