diff --git a/docs/backend.md b/docs/backend.md
index 2fdd898..51e9a6e 100644
--- a/docs/backend.md
+++ b/docs/backend.md
@@ -18,7 +18,7 @@ msgspec Structs for the kanta database. See `docs/content-model.md` for the full
## `markdown.py`
-markdown-it-py renderer (html passthrough + attrs, footnote, deflist, tasklists, admon, gfm_autolink, sub/superscript plugins; typographer + breaks on). In bodies with at least three top-level h1/h2 headings (nested ones, e.g. inside `::: aside`, never participate), each gets a slug id (`python-slugify`, mirroring the editor's `slugify.js` — unicode folds to ASCII, separators become single hyphens) unless the author set `{#id}`, and their text is wrapped in a self-link (`a.anchor`) so section links are copyable; anchored headings also carry `data-line` with their markdown source line (the page editor's section pens and piecewise scroll sync key off it); the first in-body h1 is the article title — when the markdown has no h1, `render(title=...)` injects it as `# {title}` so implicit and explicit titles take the same path — it gets no id and doesn't count toward the three, its self-link is `href=""` (scroll to top); shorter articles stay anchor-free, h3+ is never navigable, and duplicates get `-2`/`-3` suffixes. Custom 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 `
` 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).
+markdown-it-py renderer (html passthrough + attrs, footnote, deflist, tasklists, admon, gfm_autolink, sub/superscript plugins; typographer + breaks on). In bodies with at least three top-level h1/h2 headings (nested ones, e.g. inside `::: aside`, never participate), each gets a slug id (`python-slugify`, mirroring the editor's `slugify.js` — unicode folds to ASCII, separators become single hyphens) unless the author set `{#id}`, and their text is wrapped in a self-link (`a.anchor`) so section links are copyable; anchored headings also carry `data-line` with their markdown source line (the page editor's section pens and piecewise scroll sync key off it); the first in-body h1 is the article title — when the markdown has no h1, `render(title=...)` injects it as `# {title}` so implicit and explicit titles take the same path — it gets no id and doesn't count toward the three, its self-link is `href=""` (scroll to top); shorter articles stay anchor-free, h3+ is never navigable, and duplicates get `-2`/`-3` suffixes. Custom 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 `
` 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). Code fences take pandoc-style brace attributes on the info line (` ```{.python .wide #id key=val} ` — the first class is the language when no bare language word precedes the braces) as well as a trailing `{...}` line; both land on the `
`, the `` keeps only the language class.
`render()` returns a `Rendered(html, multicol)`: the article content segmented for the column layout (there is no wrapper div — segments and bare blocks are direct `` children) — h1/h2 headings, `.wide` blocks and margin-breakout blocks (`.margin`, `::: aside`) stand bare, the runs between them become `` (plus `.cols` on segments with enough text in at least two paragraphs or one long enough to split across columns, `::: nocols` opting out; in column segments, paragraphs past `BREAKABLE_TEXT` visible characters are marked `.breakable` so they may split across columns), and `multicol` flags bodies long enough to columnize (visible-text thresholds, code excluded). `views.py` puts the class on the article; pagerite.css takes it from there (at most two columns, the left-margin breakout, all viewport adaptation).
diff --git a/pagerite/markdown.py b/pagerite/markdown.py
index f802221..319f6fd 100644
--- a/pagerite/markdown.py
+++ b/pagerite/markdown.py
@@ -10,7 +10,8 @@ note/tip/warning/etc., the title optional) and GitHub-style alerts
same callout styling). ``::: name`` opens a generic container rendered
as ``
`` and closed by a matching ``:::`` (nest by
giving the outer container more colons, e.g. `::::`); the name may be
-followed by brace attributes (``::: aside {.right}``). ``::: aside``
+followed by brace attributes (``::: aside {.right}``), or omitted for a
+pandoc-style nameless div (``::: {.aside}``). ``::: aside``
floats as a muted side box, floating in the side zone at the article's
left on all but phone widths — the same margin float ``{.margin}`` (or
``::: margin``) gives any block — and ``::: nocols`` opts its section out
@@ -107,15 +108,29 @@ def _fence_rule(
) -> str:
"""Render a fenced code block.
- Like the default fence rule, but block attributes (a trailing `{...}`
- line, applied to the fence token by _block_attrs) go on the
— the
- block element — instead of the , which keeps only the language
- class. This is what makes e.g. `{.wide}` or `{style="..."}` after a
- code fence style the block itself.
+ Like the default fence rule, but block attributes go on the —
+ the block element — instead of the , which keeps only the
+ language class. Attributes are accepted both pandoc-style on the
+ info line (```{.python .wide #id key=val} — the first class is the
+ language when no bare language word precedes the braces) and as a
+ trailing `{...}` line applied by _block_attrs. This is what makes
+ e.g. `{.wide}` or `{style="..."}` style the block itself.
"""
token = tokens[idx]
info = token.info.strip() if token.info else ""
- lang = info.split(maxsplit=1)[0] if info else ""
+ lang, _, brace = info.partition("{")
+ lang = lang.split(maxsplit=1)[0] if lang.strip() else ""
+ if brace:
+ try:
+ _, attrs = parse_attrs("{" + brace)
+ except ParseError:
+ attrs = {}
+ classes = attrs.pop("class", "").split()
+ if not lang and classes:
+ lang = classes.pop(0)
+ if classes:
+ _apply_attrs(token, {"class": " ".join(classes)})
+ _apply_attrs(token, attrs)
highlighted = _highlight(token.content, lang, "") or escapeHtml(token.content)
code_class = f' class="{options.langPrefix}{lang}"' if lang else ""
return (
@@ -226,13 +241,19 @@ def _apply_attrs(token, attrs: dict) -> None:
def _container_validate(params: str, _markup: str) -> bool:
- """`::: name`, optionally followed by brace attrs (`::: aside {.right}`)."""
+ """`::: name`, optionally followed by brace attrs (`::: aside {.right}`).
+
+ Pandoc-style nameless divs (`::: {.aside}`) are accepted too — the
+ attrs alone give the container its classes.
+ """
name, _, rest = params.strip().partition(" ")
- if not _CONTAINER_NAME_RE.fullmatch(name):
+ if name.startswith("{"):
+ name, rest = "", params.strip()
+ elif not _CONTAINER_NAME_RE.fullmatch(name):
return False
rest = rest.strip()
if not rest:
- return True
+ return bool(name) # a nameless container needs the attrs
try:
pos, _ = parse_attrs(rest)
except ParseError:
@@ -253,8 +274,12 @@ def _container_attrs(state) -> None:
for token in state.tokens:
if token.type != "container_block_open":
continue
- name, _, rest = token.info.strip().partition(" ")
- token.attrJoin("class", name)
+ info = token.info.strip()
+ name, _, rest = info.partition(" ")
+ if name.startswith("{"):
+ name, rest = "", info
+ if name:
+ token.attrJoin("class", name)
if rest.strip():
_, attrs = parse_attrs(rest.strip())
_apply_attrs(token, attrs)