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)