// Derive content classification, list ordering and path building from a site's // `config.collections` (see ../../README.md). Pure functions — no config import, // no filesystem — so they unit-test against any collections array directly. // // Fed `examples/openbureau.config.js` these reproduce the engine's original // hard-coded archiv/library/rubrik/seite behaviour 1:1. // Parse a `path` pattern ('archiv/:section/:slug') into segments. function parsePattern(pattern) { return pattern.split('/').map((seg) => seg.startsWith(':') ? { param: seg.slice(1) } : { literal: seg }); } // Match a content rel-path's segments (no .md) against a `path` pattern. // Returns captured params, or null. Strict: the segment count must equal the // pattern's, so 'archiv/:section/:slug' matches exactly three deep — the same // rule the original classify() enforced for archiv. function matchPattern(segs, pattern) { const pat = parsePattern(pattern); if (segs.length !== pat.length) return null; const params = {}; for (let i = 0; i < pat.length; i++) { if (pat[i].literal !== undefined) { if (pat[i].literal !== segs[i]) return null; } else { if (!segs[i]) return null; params[pat[i].param] = segs[i]; } } return params; } // First literal segment of a pattern (e.g. 'library' from 'library/:slug'). function rootSegment(pattern) { const first = pattern.split('/')[0]; return first.startsWith(':') ? null : first; } // { kind, section } for a content file, derived from collections. // Priority: index (_index.md) → path patterns → fallback — mirrors the original // hard-coded classify() when fed openbureau.config.js. export function classify(rel, collections) { const parts = rel.split('/'); const base = parts[parts.length - 1]; const segs = rel.replace(/\.md$/, '').split('/'); // _index.md → the index collection; section = parent dir (or 'home' at root). if (base === '_index.md') { const idx = collections.find((c) => c.index); if (idx) { const section = parts.length >= 2 ? parts[parts.length - 2] : 'home'; return { kind: idx.kind, section }; } } // path-pattern collections (skip index/fallback). for (const c of collections) { if (!c.path || c.index || c.fallback) continue; const params = matchPattern(segs, c.path); if (params) { const section = params.section ?? rootSegment(c.path); return { kind: c.kind, section: section ?? null }; } } // fallback collection (everything not otherwise matched). const fb = collections.find((c) => c.fallback); return { kind: fb ? fb.kind : null, section: null }; } // Sort comparator for listEntries: collection `order`, then date desc, then title. export function compareEntries(collections) { const order = {}; collections.forEach((c, i) => { order[c.kind] = c.order ?? 100 + i; }); return (a, b) => ((order[a.kind] ?? 999) - (order[b.kind] ?? 999)) || (b.date || '').localeCompare(a.date || '') || a.title.localeCompare(b.title); } // Build a content rel-path for a NEW entry of `kind` from form `data`. // Index collection → '/_index.md'; a collection without a `path` (fallback) // → '.md'; otherwise substitute :params in the collection's `path` // (archiv/:section/:slug → archiv/
/.md). Returns '' if a required // segment is missing. export function buildPath(kind, data, collections) { const c = collections.find((x) => x.kind === kind); const slug = String(data.slug || '').trim(); if (c?.index) return slug ? `${slug}/_index.md` : ''; if (!c?.path) return slug ? `${slug}.md` : ''; let ok = true; const rel = c.path.split('/').map((seg) => { if (!seg.startsWith(':')) return seg; const v = String(data[seg.slice(1)] || '').trim(); if (!v) ok = false; return v; }).join('/'); return ok && rel ? `${rel}.md` : ''; } // Build the stats `content` object from classified entries + collections: // one counter per collection.statKey, plus a drafts counter for any collection // that declares `draftStatKey`. Mirrors the original hard-coded stats block // (beitraege/entwuerfe/library/seiten/rubriken) when fed openbureau.config.js. export function contentStats(entries, collections) { const content = {}; for (const c of collections) { if (c.statKey) content[c.statKey] ??= 0; if (c.draftStatKey) content[c.draftStatKey] ??= 0; } const byKind = Object.fromEntries(collections.map((c) => [c.kind, c])); for (const e of entries) { const c = byKind[e.kind]; if (!c) continue; if (c.statKey) content[c.statKey]++; if (c.draftStatKey && e.draft) content[c.draftStatKey]++; } return content; }