{cards}: server-rendered previews, immediate-children semantics, */** globs
Preview: the editor render handler now returns the complete article HTML
({cards} tags expanded, children's cards appended when the page has no
tag) and the frontend swaps article.innerHTML wholesale — the old
detach/keep/re-append hack for .cards (which duplicated stacks) is gone.
Semantics: one card per item, no subtree flattening (stacks removed).
Bare {cards} lists immediate children (front page: the other top-level
items); a page-less item is represented by its first leaf page, the
nav-link logic. {cards: path} renders that page alone, path/* its
direct children, path/** all published descendants.
This commit is contained in:
+1
-1
@@ -26,7 +26,7 @@ msgspec Structs for the kanta database. See `docs/content-model.md` for the full
|
|||||||
|
|
||||||
## `markdown.py`
|
## `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 `<img>` HTML stay plain. A lone `{name}` / `{name: args}` line is a block directive: a core rule turns it into a `directive` token (render instance only — the verbatim parser keeps the plain paragraph so segments/chunks see the placeholder source), and the render rule delegates to the resolvers passed as `render(directives=...)`, leaving the source literal where no resolver applies (e.g. the editor preview). Built in: `{dates}` expands to the article's published/updated dateline (`p.dateline`, from `Node.created`/`modified`, registered by `render()` when `created` is given); views.py resolves `{cards}` — the page's published children — and `{cards: path path/* ...}` (space-separated: a path's subtree as one stack, `path/*` its children as one stack each) into the same card-row markup as category pages (`.cards.wide`, a boundary block outside the column segments). A page with any `{cards}` tag drops the automatic end-of-page child cards; multiple tags each render their own row. 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 `<pre>`, the `<code>` keeps only the language class.
|
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 `<img>` HTML stay plain. A lone `{name}` / `{name: args}` line is a block directive: a core rule turns it into a `directive` token (render instance only — the verbatim parser keeps the plain paragraph so segments/chunks see the placeholder source), and the render rule delegates to the resolvers passed as `render(directives=...)`, leaving the source literal where no resolver applies (e.g. the editor preview of a page that does not exist yet). Built in: `{dates}` expands to the article's published/updated dateline (`p.dateline`, from `Node.created`/`modified`, registered by `render()` when `created` is given); views.py resolves `{cards}` — the page's published children, one card each (on the front page: the other top-level pages) — and `{cards: path path/* path/** ...}` (space-separated: a plain path renders that page alone, `path/*` its published children, `path/**` all published descendant pages; a page-less item is represented by its first leaf page, the nav-link logic) into the same card-row markup as category pages (`.cards.wide`, a boundary block outside the column segments). A page with any `{cards}` tag drops the automatic end-of-page child cards; multiple tags each render their own row. 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 `<pre>`, the `<code>` 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 `<article>` children) — h1/h2 headings and `.wide` blocks stand bare, the runs between them become `<div class="colseg">` (margin-breakout boxes — `.margin`, `::: aside` — stay inside the segment at their anchor point; the CSS positions them out of flow into the side zone) (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).
|
`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 `<article>` children) — h1/h2 headings and `.wide` blocks stand bare, the runs between them become `<div class="colseg">` (margin-breakout boxes — `.margin`, `::: aside` — stay inside the segment at their anchor point; the CSS positions them out of flow into the side zone) (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).
|
||||||
|
|
||||||
|
|||||||
@@ -736,17 +736,15 @@ function previewIntoArticle(html, multicol) {
|
|||||||
if (!article) return
|
if (!article) return
|
||||||
// The server render owns the article completely — the injected title h1,
|
// The server render owns the article completely — the injected title h1,
|
||||||
// the column layout (.multicol on the article, the .colseg/.cols
|
// the column layout (.multicol on the article, the .colseg/.cols
|
||||||
// segments) — so the whole article content swaps as one. Only the edit
|
// segments), the card stacks ({cards} tags expanded, or the children's
|
||||||
// pen and the category cards survive: detach them before innerHTML wipes
|
// cards appended when the page has no tag) — so the whole article
|
||||||
// them. pagerite.js re-places the pen into the first visible h1 on
|
// content swaps as one. Only the edit pen survives: detach it before
|
||||||
// pagerite:preview.
|
// innerHTML wipes it. pagerite.js re-places the pen into the first
|
||||||
|
// visible h1 on pagerite:preview.
|
||||||
article.classList.toggle('multicol', multicol)
|
article.classList.toggle('multicol', multicol)
|
||||||
const pen = article.querySelector('button.edit-link')
|
const pen = article.querySelector('button.edit-link')
|
||||||
if (pen) pen.remove()
|
if (pen) pen.remove()
|
||||||
const cards = article.querySelector(':scope > .cards')
|
|
||||||
if (cards) cards.remove()
|
|
||||||
article.innerHTML = html
|
article.innerHTML = html
|
||||||
if (cards) article.append(cards)
|
|
||||||
runScripts(article)
|
runScripts(article)
|
||||||
dispatchEvent(new CustomEvent('pagerite:preview'))
|
dispatchEvent(new CustomEvent('pagerite:preview'))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -481,19 +481,20 @@ main {
|
|||||||
container-type: inline-size;
|
container-type: inline-size;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Child-entry card stacks: a category page (and the content-less category
|
/* Child-entry cards: a category page (and the content-less category
|
||||||
404) lists its published children after the markdown content — one column
|
404) lists its published children after the markdown content — one
|
||||||
per child, the child's whole subtree flattened into the column in menu
|
card per child (a child without a page of its own is represented by
|
||||||
order (see _cards in views.py). The row bleeds to full page width
|
its first leaf page; see _cards in views.py). The row bleeds to full
|
||||||
(div.wide): the columns first grow to fill it, then shrink rather than
|
page width (div.wide): the cards first grow to fill it, then shrink
|
||||||
wrap. Every card has the same fixed 16/10 shape, covered entirely by the
|
rather than wrap. Every card has the same fixed 16/10 shape, covered
|
||||||
|
entirely by the
|
||||||
page's share image (og:image heuristics, as a background — a gradient
|
page's share image (og:image heuristics, as a background — a gradient
|
||||||
placeholder when it has none) with the title overlaid on a translucent
|
placeholder when it has none) with the title overlaid on a translucent
|
||||||
band at the bottom. The card is one <a> holding only phrasing-level
|
band at the bottom. The card is one <a> holding only phrasing-level
|
||||||
spans; the spans lay out as blocks. */
|
spans; the spans lay out as blocks. */
|
||||||
.cards {
|
.cards {
|
||||||
display: flex;
|
display: flex;
|
||||||
/* Stacks stop growing at their cap; center the row in the bleed then. */
|
/* Cards stop growing at their cap; center the row in the bleed then. */
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 1.25rem;
|
gap: 1.25rem;
|
||||||
margin-top: 2.5rem;
|
margin-top: 2.5rem;
|
||||||
@@ -502,18 +503,7 @@ main {
|
|||||||
padding-inline: 1.25rem;
|
padding-inline: 1.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cards .stack {
|
/* Phones: the cards stack vertically instead of shrinking to slivers.
|
||||||
/* Grow to fill the row (up to the cap — full-width rows would make huge
|
|
||||||
cards), shrink (not wrap) when there are too many. */
|
|
||||||
flex: 1 1 0;
|
|
||||||
max-width: 24rem;
|
|
||||||
min-width: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Phones: the columns stack vertically instead of shrinking to slivers.
|
|
||||||
Text-only cards (gradient cover + description) then fit their content —
|
Text-only cards (gradient cover + description) then fit their content —
|
||||||
the fixed 16/10 shape only makes sense for image covers. */
|
the fixed 16/10 shape only makes sense for image covers. */
|
||||||
@media (max-width: 48rem) {
|
@media (max-width: 48rem) {
|
||||||
@@ -527,12 +517,16 @@ main {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
|
/* Grow to fill the row (up to the cap — full-width rows would make huge
|
||||||
|
cards), shrink (not wrap) when there are too many. */
|
||||||
|
flex: 1 1 0;
|
||||||
|
max-width: 24rem;
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
aspect-ratio: 16 / 10;
|
aspect-ratio: 16 / 10;
|
||||||
/* Allow shrinking below the text's min-content width: without this the
|
/* Allow shrinking below the text's min-content width: without this the
|
||||||
text cards hold their stack wider than the image-only stacks. */
|
text cards hold their row wider than the image-only cards. */
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
|
|||||||
+25
-1
@@ -19,6 +19,7 @@ from fastapi import (
|
|||||||
WebSocket,
|
WebSocket,
|
||||||
WebSocketDisconnect,
|
WebSocketDisconnect,
|
||||||
)
|
)
|
||||||
|
from html5tagger import E
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from pagerite import i18n, views
|
from pagerite import i18n, views
|
||||||
@@ -495,6 +496,11 @@ async def editor_ws(ws: WebSocket) -> None:
|
|||||||
markdown = msg.get("markdown", "")
|
markdown = msg.get("markdown", "")
|
||||||
chain = resolve(data.menu, path)
|
chain = resolve(data.menu, path)
|
||||||
node = chain[-1] if chain else None
|
node = chain[-1] if chain else None
|
||||||
|
# Expand {cards} like page_content does, so the preview
|
||||||
|
# shows real cards, not the literal tag. No translation
|
||||||
|
# context: the preview has no lang of its own, so cards
|
||||||
|
# render in their originals.
|
||||||
|
has_cards_tag = views._CARDS_TAG_RE.search(markdown) is not None
|
||||||
rendered = render(
|
rendered = render(
|
||||||
markdown,
|
markdown,
|
||||||
path,
|
path,
|
||||||
@@ -511,12 +517,30 @@ async def editor_ws(ws: WebSocket) -> None:
|
|||||||
if node
|
if node
|
||||||
else None
|
else None
|
||||||
),
|
),
|
||||||
|
directives=(
|
||||||
|
{
|
||||||
|
"cards": lambda args, _env: views._cards_tag(
|
||||||
|
data.menu, data, node, path, args
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if node is not None and has_cards_tag
|
||||||
|
else None
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
html = rendered.html
|
||||||
|
if node is not None and not has_cards_tag:
|
||||||
|
# Without a {cards} tag page_content appends the
|
||||||
|
# children's cards after the content — the preview
|
||||||
|
# replaces the whole article, so include them here.
|
||||||
|
doc = E.div
|
||||||
|
with doc:
|
||||||
|
views._cards(doc, data.menu, data, node, path)
|
||||||
|
html += str(doc)
|
||||||
await ws.send_json(
|
await ws.send_json(
|
||||||
{
|
{
|
||||||
"type": "html",
|
"type": "html",
|
||||||
"path": path,
|
"path": path,
|
||||||
"html": rendered.html,
|
"html": html,
|
||||||
# Column-layout flag: the preview toggles the
|
# Column-layout flag: the preview toggles the
|
||||||
# article's .multicol class and swaps in the
|
# article's .multicol class and swaps in the
|
||||||
# segmented (.colseg/.cols) article html.
|
# segmented (.colseg/.cols) article html.
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ classes, e.g. `{.right}`.
|
|||||||
|
|
||||||
A lone `{name}` or `{name: args}` line is a block directive, expanded by
|
A lone `{name}` or `{name: args}` line is a block directive, expanded by
|
||||||
the caller through render(directives=...) — `{dates}` (built in) expands
|
the caller through render(directives=...) — `{dates}` (built in) expands
|
||||||
to the article's dateline, `{cards}` / `{cards: path ...}` to card stacks
|
to the article's dateline, `{cards}` / `{cards: path ...}` to card rows
|
||||||
of other pages (views.py). Unresolved tags render as the literal source.
|
of other pages (views.py). Unresolved tags render as the literal source.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -522,7 +522,7 @@ def _directives(state) -> None:
|
|||||||
The expansion is not markdown.py's business: _directive_rule delegates
|
The expansion is not markdown.py's business: _directive_rule delegates
|
||||||
to the resolvers render() put in env["directives"], falling back to the
|
to the resolvers render() put in env["directives"], falling back to the
|
||||||
literal source when the tag is unknown in the context (e.g. the editor
|
literal source when the tag is unknown in the context (e.g. the editor
|
||||||
preview without page data). The ``cards`` directive gets .wide so it
|
preview of a page that does not exist yet). The ``cards`` directive gets .wide so it
|
||||||
stands alone as a full-width block outside the column segments (the
|
stands alone as a full-width block outside the column segments (the
|
||||||
card markup never flows in columns). Runs on the render instance only —
|
card markup never flows in columns). Runs on the render instance only —
|
||||||
the verbatim parser keeps the plain paragraph so segments/chunks see
|
the verbatim parser keeps the plain paragraph so segments/chunks see
|
||||||
@@ -754,7 +754,8 @@ def render(
|
|||||||
``{name}`` or ``{name: args}`` line — are expanded by the resolvers
|
``{name}`` or ``{name: args}`` line — are expanded by the resolvers
|
||||||
passed as ``directives`` (name → (args, env) → HTML or None), with
|
passed as ``directives`` (name → (args, env) → HTML or None), with
|
||||||
``dates`` built in when ``created`` is given; unresolved tags render as
|
``dates`` built in when ``created`` is given; unresolved tags render as
|
||||||
the literal source (e.g. in the editor preview without page data).
|
the literal source (e.g. in the editor preview of a not-yet-created
|
||||||
|
page).
|
||||||
Position is the author's choice — the dateline typically goes
|
Position is the author's choice — the dateline typically goes
|
||||||
right after the article's h1.
|
right after the article's h1.
|
||||||
"""
|
"""
|
||||||
|
|||||||
+71
-57
@@ -792,8 +792,9 @@ def page_content(
|
|||||||
|
|
||||||
A page with published children (a category page) lists them as cards
|
A page with published children (a category page) lists them as cards
|
||||||
after the markdown content — unless the content has a ``{cards}`` tag,
|
after the markdown content — unless the content has a ``{cards}`` tag,
|
||||||
which places card stacks itself (bare: the children; with paths:
|
which places card rows itself (bare: the children; with paths:
|
||||||
those pages / ``path/*`` their children), one row per tag. With a translation, its Markdown goes
|
those pages, ``path/*`` their children, ``path/**`` all descendants),
|
||||||
|
one row per tag. With a translation, its Markdown goes
|
||||||
through the same render pipeline; missing pieces (markdown=None, absent
|
through the same render pipeline; missing pieces (markdown=None, absent
|
||||||
title entries) fall back to the original. ``lang`` feeds the cards'
|
title entries) fall back to the original. ``lang`` feeds the cards'
|
||||||
per-target localization.
|
per-target localization.
|
||||||
@@ -815,7 +816,7 @@ def page_content(
|
|||||||
)
|
)
|
||||||
# The title is injected into the markdown (as # title when it has no
|
# The title is injected into the markdown (as # title when it has no
|
||||||
# h1 of its own), so title and content render as one article.
|
# h1 of its own), so title and content render as one article.
|
||||||
# A {cards} tag places the card stacks itself (possibly several);
|
# A {cards} tag places the card rows itself (possibly several);
|
||||||
# without one the children are appended after the content as before.
|
# without one the children are appended after the content as before.
|
||||||
has_cards_tag = _CARDS_TAG_RE.search(content) is not None
|
has_cards_tag = _CARDS_TAG_RE.search(content) is not None
|
||||||
directives = None
|
directives = None
|
||||||
@@ -846,6 +847,20 @@ def page_content(
|
|||||||
return HTML(str(doc))
|
return HTML(str(doc))
|
||||||
|
|
||||||
|
|
||||||
|
def _represent(node: Node, path: str) -> tuple[str, Node] | None:
|
||||||
|
"""The (path, node) a card for this menu item points at: the item
|
||||||
|
itself when it has a page, else its first published leaf page,
|
||||||
|
recursively — the same logic as nav links (first_leaf)."""
|
||||||
|
if node.chunks:
|
||||||
|
return path, node
|
||||||
|
for slug, child in sorted_nodes(node.children):
|
||||||
|
if child.published:
|
||||||
|
cpath = f"{path}/{slug}" if path else slug
|
||||||
|
if r := _represent(child, cpath):
|
||||||
|
return r
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _cards(
|
def _cards(
|
||||||
doc,
|
doc,
|
||||||
menu: dict[str, Node],
|
menu: dict[str, Node],
|
||||||
@@ -856,49 +871,34 @@ def _cards(
|
|||||||
link_lang: str = "",
|
link_lang: str = "",
|
||||||
lang: str = "",
|
lang: str = "",
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Card stacks of the node's published children (nothing when childless).
|
"""Cards of the node's published children (nothing when childless).
|
||||||
|
|
||||||
One column per direct child, all in a single full-width row (the .wide
|
One card per direct child, all in a single full-width row (the .wide
|
||||||
breakout): the columns grow to fill the page and shrink rather than
|
breakout): the cards grow to fill the page and shrink rather than
|
||||||
wrap. A column holds the child's whole subtree flattened in menu order
|
wrap. A child without a page of its own is represented by its first
|
||||||
— nesting levels are not split out — starting with the first page that
|
leaf page (_represent, the nav-link logic). Each card is one <a>
|
||||||
has actual content (the child itself when it does, its first leaf
|
showing the page's share
|
||||||
otherwise, recursively). Each card is one <a> showing the page's share
|
|
||||||
image (the same heuristics as og:image) as the cover and its title;
|
image (the same heuristics as og:image) as the cover and its title;
|
||||||
image-less cards get a gradient cover and also show the description.
|
image-less cards get a gradient cover and also show the description.
|
||||||
Only phrasing-level elements (spans) go inside the <a>: as a formatting
|
Only phrasing-level elements (spans) go inside the <a>: as a formatting
|
||||||
element it would be cloned by the HTML parser around any block-level
|
element it would be cloned by the HTML parser around any block-level
|
||||||
child, splitting one card into several links.
|
child, splitting one card into several links.
|
||||||
"""
|
"""
|
||||||
items = [(s, c) for s, c in sorted_nodes(node.children) if c.published]
|
items = [
|
||||||
|
r
|
||||||
|
for s, c in sorted_nodes(node.children)
|
||||||
|
if c.published
|
||||||
|
for r in [_represent(c, f"{path}/{s}" if path else s)]
|
||||||
|
if r
|
||||||
|
]
|
||||||
if not items:
|
if not items:
|
||||||
return
|
return
|
||||||
with doc.div(class_="cards wide"):
|
with doc.div(class_="cards wide"):
|
||||||
for slug, child in items:
|
for cpath, cnode in items:
|
||||||
cpath = f"{path}/{slug}" if path else slug
|
_card(doc, data, cnode, cpath, translation, link_lang, lang)
|
||||||
_card_stack(doc, data, cpath, child, translation, link_lang, lang)
|
|
||||||
|
|
||||||
|
|
||||||
def _card_stack(
|
#: A lone {cards} or {cards: ...} line in the markdown: card rows placed
|
||||||
doc,
|
|
||||||
data: Data,
|
|
||||||
path: str,
|
|
||||||
node: Node,
|
|
||||||
translation: Translation | None = None,
|
|
||||||
link_lang: str = "",
|
|
||||||
lang: str = "",
|
|
||||||
) -> None:
|
|
||||||
"""One stack column: the subtree of ``node`` flattened in menu order
|
|
||||||
(see _cards)."""
|
|
||||||
entries = list(_walk(node, path))
|
|
||||||
if not entries:
|
|
||||||
return
|
|
||||||
with doc.div(class_="stack"):
|
|
||||||
for epath, enode in entries:
|
|
||||||
_card(doc, data, enode, epath, translation, link_lang, lang)
|
|
||||||
|
|
||||||
|
|
||||||
#: A lone {cards} or {cards: ...} line in the markdown: card stacks placed
|
|
||||||
#: by the author. Any such tag suppresses the automatic end-of-page cards.
|
#: by the author. Any such tag suppresses the automatic end-of-page cards.
|
||||||
_CARDS_TAG_RE = re.compile(r"^\{cards(?::[^{}\n]*)?\}[ \t]*$", re.M)
|
_CARDS_TAG_RE = re.compile(r"^\{cards(?::[^{}\n]*)?\}[ \t]*$", re.M)
|
||||||
|
|
||||||
@@ -917,47 +917,61 @@ def _cards_tag(
|
|||||||
_cards, so author-placed cards look like category cards).
|
_cards, so author-placed cards look like category cards).
|
||||||
|
|
||||||
A bare ``{cards}`` lists the page's own published children — what
|
A bare ``{cards}`` lists the page's own published children — what
|
||||||
page_content appends when the tag is absent. Arguments are
|
page_content appends when the tag is absent — or, on the front page,
|
||||||
space-separated page paths: a path contributes its subtree (one stack,
|
the other top-level pages (the front page is a top-level item itself,
|
||||||
flattened like a category child), and ``path/*`` its published
|
not the parent of the others). Arguments are space-separated page
|
||||||
children (one stack each). Unresolvable paths are skipped; a tag that
|
paths: a plain path renders that page alone (never its children),
|
||||||
ends up with nothing renders as nothing.
|
``path/*`` its published children and ``path/**`` all published
|
||||||
|
descendant pages. One card per item; a page-less item is represented
|
||||||
|
by its first leaf page (_represent). Unresolvable paths are skipped;
|
||||||
|
a tag that ends up with nothing renders as nothing.
|
||||||
"""
|
"""
|
||||||
items: list[tuple[str, Node]] = []
|
items: list[tuple[str, Node]] = []
|
||||||
specs = args.split()
|
specs = args.split()
|
||||||
|
|
||||||
|
def children(base: str, parent: Node):
|
||||||
|
for s, c in sorted_nodes(parent.children):
|
||||||
|
if c.published:
|
||||||
|
if r := _represent(c, f"{base}/{s}" if base else s):
|
||||||
|
items.append(r)
|
||||||
|
|
||||||
if not specs:
|
if not specs:
|
||||||
items = [
|
if path:
|
||||||
(f"{path}/{s}" if path else s, c)
|
children(path, node)
|
||||||
for s, c in sorted_nodes(node.children)
|
else:
|
||||||
if c.published
|
for s, c in sorted_nodes(menu):
|
||||||
]
|
if c.published and s:
|
||||||
|
if r := _represent(c, s):
|
||||||
|
items.append(r)
|
||||||
else:
|
else:
|
||||||
for spec in specs:
|
for spec in specs:
|
||||||
spec = spec.strip("/")
|
spec = spec.strip("/")
|
||||||
if spec.endswith("/*"):
|
if spec.endswith("/**"):
|
||||||
|
base = spec[:-3].rstrip("/")
|
||||||
|
if chain := resolve(menu, base):
|
||||||
|
for s, c in sorted_nodes(chain[-1].children):
|
||||||
|
if c.published:
|
||||||
|
items.extend(_walk(c, f"{base}/{s}" if base else s))
|
||||||
|
elif spec.endswith("/*"):
|
||||||
base = spec[:-2].rstrip("/")
|
base = spec[:-2].rstrip("/")
|
||||||
chain = resolve(menu, base)
|
if chain := resolve(menu, base):
|
||||||
if chain:
|
children(base, chain[-1])
|
||||||
items.extend(
|
|
||||||
(f"{base}/{s}" if base else s, c)
|
|
||||||
for s, c in sorted_nodes(chain[-1].children)
|
|
||||||
if c.published
|
|
||||||
)
|
|
||||||
elif chain := resolve(menu, spec):
|
elif chain := resolve(menu, spec):
|
||||||
items.append((spec, chain[-1]))
|
if r := _represent(chain[-1], spec):
|
||||||
|
items.append(r)
|
||||||
if not items:
|
if not items:
|
||||||
return ""
|
return ""
|
||||||
doc = E.div(class_="cards wide")
|
doc = E.div(class_="cards wide")
|
||||||
with doc:
|
with doc:
|
||||||
for cpath, cnode in items:
|
for cpath, cnode in items:
|
||||||
_card_stack(doc, data, cpath, cnode, translation, link_lang, lang)
|
_card(doc, data, cnode, cpath, translation, link_lang, lang)
|
||||||
return str(doc)
|
return str(doc)
|
||||||
|
|
||||||
|
|
||||||
def _walk(node: Node, path: str):
|
def _walk(node: Node, path: str):
|
||||||
"""Published content pages of a subtree, pre-order in menu order: the
|
"""Published content pages of a subtree, pre-order in menu order: the
|
||||||
node itself first when it has content (the stack's landing card), then
|
node itself first when it has content, then its descendants
|
||||||
its descendants (content-less nodes contribute only their subtree)."""
|
(content-less nodes contribute only their subtree)."""
|
||||||
if node.chunks:
|
if node.chunks:
|
||||||
yield path, node
|
yield path, node
|
||||||
for slug, child in sorted_nodes(node.children):
|
for slug, child in sorted_nodes(node.children):
|
||||||
@@ -974,7 +988,7 @@ def _card(
|
|||||||
link_lang: str = "",
|
link_lang: str = "",
|
||||||
lang: str = "",
|
lang: str = "",
|
||||||
) -> None:
|
) -> None:
|
||||||
"""One card in a stack: cover + title, plus the description when the
|
"""One card: cover + title, plus the description when the
|
||||||
page has no image (its card shows a gradient cover instead).
|
page has no image (its card shows a gradient cover instead).
|
||||||
|
|
||||||
The card text localizes per target article where that page is
|
The card text localizes per target article where that page is
|
||||||
|
|||||||
Reference in New Issue
Block a user