-- 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 } 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 } -- ─── 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 alternate_links: List -- hreflang alternates for translations menu_items: List post_categories: List post_tags: List tag_color_by_name: Map backlinks: List canonical_post_path_by_slug: Map canonical_media_path_by_source_path: Map 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). }