Squashed 'cms/core/' content from commit 59b9e20
git-subtree-dir: cms/core git-subtree-split: 59b9e2075ac48cf017db38009d54dbe56a98fffc
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
// 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 → '<slug>/_index.md'; a collection without a `path` (fallback)
|
||||
// → '<slug>.md'; otherwise substitute :params in the collection's `path`
|
||||
// (archiv/:section/:slug → archiv/<section>/<slug>.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;
|
||||
}
|
||||
Reference in New Issue
Block a user