Files
bDS2/specs/template.allium
2026-04-23 10:42:27 +02:00

212 lines
7.4 KiB
Plaintext

-- allium: 1
-- bDS Liquid Template System
-- Scope: core (Wave 1 data, Wave 4 rendering)
-- Distilled from: src/main/engine/TemplateEngine.ts, PageRenderer.ts, schema.ts,
-- bundled starter templates in src/main/engine/templates/
entity Template {
slug: String
title: String
kind: post | list | not_found | partial
enabled: Boolean
status: draft | published
content: String?
version: Integer
file_path: String
created_at: Timestamp
updated_at: Timestamp
-- Derived
content_location: if status = published: file_path else: content
referencing_posts: Posts where template_slug = this.slug
referencing_tags: Tags where post_template_slug = this.slug
transitions status {
draft -> published
published -> draft
}
}
surface TemplateManagementSurface {
facing _: TemplateOperator
provides:
CreateTemplateRequested(title, kind, content)
CreateAndPublishTemplateRequested(title, kind, content)
UpdateTemplateRequested(template, changes)
PublishTemplateRequested(template)
DeleteTemplateRequested(template)
RebuildTemplatesFromFilesRequested(project)
}
invariant UniqueTemplateSlug {
for a in Templates:
for b in Templates:
a != b implies a.slug != b.slug
}
invariant TemplateFrontmatter {
-- .liquid files use standard --- YAML frontmatter
-- Fields: id, slug, title, kind, enabled, version, createdAt, updatedAt
for t in Templates where status = published:
parse_frontmatter(read_file(t.file_path)).slug = t.slug
}
invariant TemplateFileLayout {
for t in Templates where file_path != "":
t.file_path = format("templates/{slug}.liquid", slug: t.slug)
}
rule CreateTemplate {
when: CreateTemplateRequested(title, kind, content)
let slug = slugify(title)
-- Creates a draft template: content stored in DB, no file written yet
ensures:
let new_template = Template.created(
slug: slug,
title: title,
kind: kind,
content: content,
status: draft,
enabled: true,
version: 1,
file_path: ""
)
new_template.status = draft
}
rule CreateAndPublishTemplate {
-- Alternative creation path: create + immediately publish (file written)
-- Some implementations may expose this as a single user action
when: CreateAndPublishTemplateRequested(title, kind, content)
let slug = slugify(title)
requires: ValidateLiquid(content) = valid
ensures:
let new_template = Template.created(
slug: slug,
title: title,
kind: kind,
content: null,
status: published,
enabled: true,
version: 1,
file_path: format("templates/{slug}.liquid", slug: slug)
)
TemplateFileWritten(new_template)
}
rule UpdateTemplate {
when: UpdateTemplateRequested(template, changes)
ensures: TemplateFieldsUpdated(template, changes)
ensures: template.updated_at = now
ensures: template.version = template.version + 1
}
rule ReopenPublishedTemplate {
when: UpdateTemplateRequested(template, changes)
requires: template.status = published
requires: template_changes_affect_rendered_output(changes)
ensures: template.status = draft
}
rule PublishTemplate {
when: PublishTemplateRequested(template)
requires: template.status = draft
requires: ValidateLiquid(template.content) = valid
-- The template parser must accept the template
ensures: template.status = published
ensures: TemplateFileWritten(template)
-- Writes frontmatter + liquid to templates/{slug}.liquid
ensures: template.content = null
}
rule DeleteTemplate {
when: DeleteTemplateRequested(template)
requires: template.referencing_posts.count = 0
requires: template.referencing_tags.count = 0
-- Cannot delete a template still referenced by posts or tags
ensures: not exists template
ensures: TemplateFileDeleted(template)
}
rule CascadeSlugUpdate {
when: template: Template.slug transitions_to new_slug
-- When a template slug changes, update all references
for p in template.referencing_posts:
ensures: p.template_slug = new_slug
for t in template.referencing_tags:
ensures: t.post_template_slug = new_slug
}
rule RebuildTemplatesFromFiles {
when: RebuildTemplatesFromFilesRequested(project)
for file in scan_directory(project.effective_data_dir + "/templates", "*.liquid"):
let parsed = parse_template_file(file)
ensures: Template.created(parsed)
-- or updated if slug already exists
}
-- Exact Liquid subset required (distilled from bundled starter templates)
-- No features beyond this list are used.
invariant LiquidTagSubset {
-- Only these 5 tags are used:
-- {% if %} / {% elsif %} / {% else %} / {% endif %}
-- {% for %} / {% endfor %}
-- {% assign %}
-- {% render 'partial', named_param: value %} (with named parameters)
-- Whitespace-stripped variants: {%- -%}
--
-- NOT used: include, capture, case/when, unless, raw, comment,
-- cycle, tablerow, increment, decrement, liquid, echo
}
invariant LiquidFilterSubset {
-- Standard filters (4):
-- | escape
-- | url_encode
-- | default: fallback_value
-- | append: suffix_string
--
-- Custom filters (2):
-- | i18n: language — translates a key string for given language
-- | markdown: post_id, post_data_json_by_id, canonical_post_path_by_slug,
-- canonical_media_path_by_source_path, language, language_prefix
-- — renders Markdown to HTML with link rewriting (6 arguments)
--
-- NOT used: date, strip_html, truncate, split, join, size (as filter),
-- upcase, downcase, replace, remove, sort, map, where, first, last,
-- reverse, concat, uniq, compact, strip, newline_to_br, json, prepend,
-- and all math filters
}
invariant LiquidOperatorSubset {
-- Comparison: ==, >
-- Logical: or, and
-- Truthy/falsy: bare variable in {% if variable %}
-- Special values: blank (nil/empty comparison)
-- Property access: dot notation (object.property), .size on arrays,
-- bracket notation for map lookups (map[key])
}
invariant LiquidRenderContext {
-- Template rendering context provides these top-level variables:
-- language, language_prefix, html_theme_attribute,
-- page_title, pico_stylesheet_href,
-- blog_languages (array of {is_current, code, flag, href_prefix}),
-- alternate_links (array of {hreflang, href}),
-- menu_items (tree of {href, title, has_children, children}),
-- calendar_initial_year, calendar_initial_month,
-- post (single post context: {title, content, id, slug, show_title}),
-- post_categories, post_tags, tag_color_by_name (map),
-- backlinks (array of {path, display_slug}),
-- day_blocks (array of {show_date_marker, date_label, posts, show_separator}),
-- archive_context ({kind, name, month, year, day}),
-- show_archive_range_heading, min_date, max_date,
-- canonical_post_path_by_slug (map), canonical_media_path_by_source_path (map),
-- post_data_json_by_id (map),
-- is_list_page, is_first_page, is_last_page,
-- has_prev_page, has_next_page, prev_page_href, next_page_href,
-- not_found_message, not_found_back_label
}