Layout: breakable paragraphs across columns, sidebar gets its own flexible track at every width, cols requires multiple paragraphs; h3 size fix in corporate theme

This commit is contained in:
2026-08-28 23:23:51 +00:00
parent 5101a5c5bf
commit 22c4f0e752
5 changed files with 188 additions and 87 deletions
+60 -12
View File
@@ -27,12 +27,16 @@ render() also builds the layout structure: the top-level blocks are
segmented for the column layout — h1/h2 headings, ``.wide`` blocks and
margin-breakout blocks (``.margin``, ``::: aside``) stand on their own,
the runs between them are wrapped in ``<div class="colseg">`` (tagged
``.cols`` when the segment holds enough text, unless a ``::: nocols``
container opts it out). The result carries ``multicol`` when the whole
body justifies columns (views.py puts the class on the article); how
many columns (never more than two), whether the margin breakout applies
and every other viewport adaptation is then pagerite.css's call. The
thresholds measure visible text, code blocks excluded.
``.cols`` when the segment holds enough text — COLS_TEXT — in at least
COLS_PARAS paragraphs or one paragraph long enough to turn .breakable,
unless a ``::: nocols`` container opts it out;
in column segments, paragraphs past BREAKABLE_TEXT are marked
``.breakable`` so they may split across columns). The result carries
``multicol`` when the whole body justifies columns (views.py puts the
class on the article); how many columns (never more than two), whether
the margin breakout applies and every other viewport adaptation is then
pagerite.css's call. The thresholds measure visible text, code blocks
excluded.
markdown-it's typographer is enabled, so body text gets SmartyPants-style
replacements: straight quotes become curly, ``--`` / ``---`` become en / em
@@ -434,12 +438,23 @@ md.core.ruler.push("heading_ids", _heading_ids)
# Text-length thresholds (visible characters, code blocks excluded) for the
# column layout: the article goes .multicol past MULTICOL_TEXT, and a column
# segment gets .cols past COLS_TEXT.
# segment gets .cols past COLS_TEXT — provided it also has at least
# COLS_PARAS paragraphs or a paragraph long enough to turn .breakable: a
# lone unbreakable paragraph would fill a column on its own and strand the
# rest (e.g. a floated figure) in the other, leaving a mostly empty column.
MULTICOL_TEXT = 1800
COLS_TEXT = 600
COLS_PARAS = 2
#: Paragraphs past this visible length are marked .breakable, letting them
#: split across columns (shorter ones stay unbreakable so a paragraph never
#: straddles the column gap).
BREAKABLE_TEXT = 800
_PRE_BLOCK_RE = re.compile(r"<pre\b.*?</pre>", re.S)
_TAG_RE = re.compile(r"<[^>]+>")
_PARA_OPEN_RE = re.compile(r"<p[\s>]")
_PARA_RE = re.compile(r"<p((?:\s[^>]*)?)>(.*?)</p>", re.S)
# Classes that take their block out of the column flow: .wide is a
# full-width separator, .margin/.aside float in the side zone at the
@@ -466,6 +481,28 @@ def _text_len(html: str) -> int:
return len(_TAG_RE.sub("", _PRE_BLOCK_RE.sub("", html)).strip())
def _breakable_paras(html: str) -> str:
"""Mark column-filling paragraphs .breakable so they may split.
Columns keep paragraphs whole (break-inside: avoid-column), but a
paragraph long enough to fill a column would strand everything after
it in a column of its own — these get .breakable, and pagerite.css
lets them split across the column gap. Only applied to .cols segments.
"""
def repl(m: re.Match[str]) -> str:
attrs, body = m.group(1), m.group(2)
if _text_len(body) <= BREAKABLE_TEXT:
return m.group(0)
if 'class="' in attrs:
attrs = attrs.replace('class="', 'class="breakable ', 1)
else:
attrs = f'{attrs} class="breakable"'
return f"<p{attrs}>{body}</p>"
return _PARA_RE.sub(repl, html)
def _top_level_blocks(tokens: list) -> list[list]:
"""Split the token stream into its top-level blocks.
@@ -520,10 +557,12 @@ def render(
The top-level blocks are grouped into column segments: boundary blocks
(h1/h2 headings, .wide, margin-breakout blocks — see _is_boundary) are
rendered bare, the runs between them wrapped in <div class="colseg">.
A segment is tagged .cols when it holds enough text (COLS_TEXT) and no
::: nocols container; the article is .multicol when the whole body
exceeds MULTICOL_TEXT. pagerite.css keys all column and margin-breakout
layout off these classes.
A segment is tagged .cols when it holds enough text (COLS_TEXT) in at
least two paragraphs (COLS_PARAS) or one breakable-length paragraph,
and no ::: nocols container; its long paragraphs are marked .breakable;
the article is .multicol when the whole body exceeds MULTICOL_TEXT.
pagerite.css keys all column and margin-breakout layout off these
classes.
A ``{dates}`` line expands to the article's published/updated dateline
(needs ``created``/``modified``; left as-is in contexts without them,
@@ -562,7 +601,16 @@ def render(
nocols = any(
"nocols" in _classes(t) for t in group if t.type == "container_block_open"
)
cols = " cols" if text_len > COLS_TEXT and not nocols else ""
marked = _breakable_paras(html)
cols = (
" cols"
if text_len > COLS_TEXT
and not nocols
and (len(_PARA_OPEN_RE.findall(html)) >= COLS_PARAS or marked != html)
else ""
)
if cols:
html = marked
parts.append(f'<div class="colseg{cols}">{html}</div>')
html = "".join(parts)
if created is not None and "<p>{dates}</p>" in html:
-1
View File
@@ -106,7 +106,6 @@ article h2 {
article h3 {
font-weight: 700;
font-size: 0.95rem;
letter-spacing: 0.08em;
color: var(--muted);
}