bba1df32cb
git-subtree-dir: cms/core git-subtree-split: 59b9e2075ac48cf017db38009d54dbe56a98fffc
120 lines
4.6 KiB
JavaScript
120 lines
4.6 KiB
JavaScript
// 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;
|
|
}
|