B1-5..B1-20: distill remaining code behaviors into specs (rendering.allium, post/media/task/generation/editor specs)

This commit is contained in:
2026-05-30 14:33:19 +02:00
parent dfb2f8870b
commit 723a7ec1f7
11 changed files with 354 additions and 22 deletions

207
specs/rendering.allium Normal file
View File

@@ -0,0 +1,207 @@
-- allium: 1
-- bDS Rendering Subsystem
-- Scope: core — template rendering shared by preview and generation
-- Distilled from: lib/bds/rendering/{filters,labels,links_and_languages,
-- metadata,post_rendering,list_archive}.ex
-- The rendering subsystem turns a post/list/not-found record plus project
-- metadata into the assigns consumed by a Liquid template. It is shared by the
-- preview server (on-demand) and static site generation (published .md files).
-- Rendering language is the CONTENT language (post/project), never the UI locale.
use "./template.allium" as template
use "./template_context.allium" as template_context
use "./i18n.allium" as i18n
use "./post.allium" as post
-- ─── Custom Liquid filters ───────────────────────────────────
-- The three custom filters available in the Liquid subset (see
-- template.allium LiquidFilterSubset). Applied during template rendering.
surface RenderFilterSurface {
facing _: TemplateRenderer
provides:
I18nFilterApplied(value, language)
MarkdownFilterApplied(value, post_id, language)
SlugifyFilterApplied(value)
}
rule I18nFilter {
when: I18nFilterApplied(value, language)
-- {{ "Key" | i18n }} -> localized "render" domain string for `language`.
-- Empty/blank input returns empty string.
ensures: result = lgettext(language, "render", trim(value))
}
rule SlugifyFilter {
when: SlugifyFilterApplied(value)
-- {{ title | slugify }} -> URL-friendly slug (same Slug.slugify as posts).
ensures: result = slugify(value)
}
rule MarkdownFilter {
when: MarkdownFilterApplied(value, post_id, language)
-- {{ body | markdown }} pipeline, in order:
-- 1. Expand built-in [[...]] macros (see ExpandBuiltinMacros)
-- 2. Convert markdown to HTML (Earmark; errors degrade to partial HTML)
-- 3. Rewrite href/src URLs to canonical post/media paths (see RewriteUrls)
ensures: result = rewritten_html
}
-- ─── Built-in macros ─────────────────────────────────────────
-- Macros use double-bracket syntax [[name param="value" ...]], expanded in
-- markdown before HTML conversion. NOT Liquid tags. Each renders a bundled
-- macro template (macros/{name}) in an isolated Liquid subscope.
value BuiltinMacro {
name: String -- youtube | vimeo | gallery | photo_archive | tag_cloud
params: Map<String, String>
}
surface BuiltinMacroSurface {
context macro: BuiltinMacro
exposes:
macro.name
macro.params
}
rule ExpandBuiltinMacros {
when: MacroEncountered(macro, language, post_id)
-- Unknown macro names are left verbatim in the source.
if macro.name = "youtube" or macro.name = "vimeo":
-- Embeds video by `id`; localized default title when title param absent.
ensures: MacroTemplateRendered(format("macros/{name}", name: macro.name))
if macro.name = "gallery":
-- Image gallery from the post's linked image media (ordered by sort).
-- `columns` clamped to 1..6 (default 3). Empty when no post_id/images.
ensures: MacroTemplateRendered("macros/gallery")
if macro.name = "photo_archive":
-- Month-grouped image archive for the project (optional year/month filter).
ensures: MacroTemplateRendered("macros/photo-archive")
if macro.name = "tag_cloud":
-- Weighted tag cloud from post tag counts; per-tag colours from Tag rows.
ensures: MacroTemplateRendered("macros/tag-cloud")
}
invariant MacroIsolation {
-- Macro templates render in an isolated Liquid subscope so macro-local
-- assigns never leak into the surrounding template context.
}
-- ─── URL rewriting ───────────────────────────────────────────
rule RewriteUrls {
when: RenderedHtmlProduced(html, canonical_post_paths, canonical_media_paths)
-- Rewrites href= and src= attribute values in rendered HTML.
-- External/special URLs (scheme:, //, #) are left untouched.
-- Internal post references (/post/{slug}, /posts/{slug}, dated paths) map to
-- the post's canonical dated path; query/fragment suffixes are preserved.
-- Internal /media/YYYY/MM/{file} references map to canonical media paths.
ensures: AttributesRewritten(html)
}
-- ─── Links and languages ─────────────────────────────────────
value LinkContext {
href: String
title: String
display_slug: String
language: String
}
rule ResolveLanguagePrefix {
when: LanguagePrefixRequested(language, main_language)
-- "" for the main language (and nil/blank); "/{language}" otherwise.
if language = main_language:
ensures: prefix = ""
else:
ensures: prefix = format("/{lang}", lang: language)
}
rule CollectLinkContexts {
when: LinkContextsRequested(project, post_id, direction)
-- direction = incoming (backlinks) | outgoing.
-- One LinkContext per linked post that still exists; missing targets dropped.
-- href = canonical post path, language normalized to main when unset.
ensures: List<LinkContext>
}
-- ─── Render labels ───────────────────────────────────────────
value RenderLabels {
-- Localized strings for rendered/preview output, resolved in the "render"
-- gettext domain for the CONTENT language (not the UI locale). Includes
-- taxonomy, backlinks, archive, pagination, calendar, search, not-found,
-- and macro fallback labels. Month names resolved 1..12 per language.
}
invariant LabelsUseContentLanguage {
-- RenderLabels and the i18n filter resolve against the content/render
-- language, consistent with i18n.allium's split-localization rule.
}
-- ─── Post render assigns ─────────────────────────────────────
-- The full assigns map for a single post template, assembled from the post
-- record (Post or PostTranslation) and project metadata.
value PostRenderAssigns {
language: String
language_prefix: String
page_title: String?
pico_stylesheet_href: String
blog_languages: List<i18n/RenderLanguage>
alternate_links: List<AlternateLink> -- hreflang alternates for translations
menu_items: List<template_context/MenuItem>
post_categories: List<String>
post_tags: List<String>
tag_color_by_name: Map<String, String?>
backlinks: List<LinkContext>
canonical_post_path_by_slug: Map<String, String>
canonical_media_path_by_source_path: Map<String, String>
post_data_json_by_id: String -- PostData JSON for client widgets
post: template_context/PostContext -- includes incoming/outgoing links
labels: RenderLabels
calendar_initial_year: Integer?
calendar_initial_month: Integer?
}
value AlternateLink {
language: String
href: String
}
rule BuildPostAssigns {
when: PostAssignsRequested(project, assigns)
-- Loads the post/translation record, renders its markdown body (macros +
-- HTML + URL rewrite), collects incoming/outgoing links, and resolves all
-- metadata-derived assigns (menu, languages, alternates, tag colours,
-- calendar bounds, labels) for the post's language.
ensures: PostRenderAssigns
}
rule BuildNotFoundAssigns {
when: NotFoundAssignsRequested(project, assigns)
-- Assigns for the 404 page: page_title defaults to "404", no alternates,
-- but shares language/menu/blog_languages/stylesheet/labels with posts.
-- Consumed by the not-found template (see generation.allium 404.html).
ensures: NotFoundRenderAssigns
}
rule BuildListAssigns {
when: ListAssignsRequested(project, assigns)
-- Assigns for list/archive pages (home, category, tag, date archives):
-- paginated post list plus shared metadata-derived assigns.
ensures: ListRenderAssigns
}
invariant SharedRenderPathForPreviewAndGeneration {
-- Preview and generation produce identical HTML for the same input because
-- both build assigns through this subsystem and render via the same Liquid
-- subset. They differ only in content SOURCE (see preview.allium
-- PreviewDraftOverlay and generation.allium GenerationPublishedOnly).
}