/*
RFC-0220 Material Credits helpers shared by UI and Site OS commands. Parses sidecar
YAML records, resolves localized/default-language fallback, and builds simple
schema.org CreativeWork-style payloads from the same structured source.
- Do not scan the filesystem; callers supply raw sidecar maps.
- Do not render UI; @warpgogol/werkstatt-site/ui owns presentation.
- RFC-0220: initial helper surface for material credit sidecars.
- RFC-0227: buildMaterialCreditJsonLd emits @id for page-graph linking.
*/
import { parse as parseYaml } from "yaml";
import {
type MaterialCredit,
type MaterialCreditLabels,
type MaterialKind,
type MaterialTarget,
materialCreditSchema,
materialTargetKey,
formatMaterialCreditLine,
} from "../schemas/material-credit.ts";
// RFC-0488: re-export label mapping helpers for @warpgogol/werkstatt-shared/attribution/material-credits consumers.
export {
labelForSourceType,
labelForStatus,
labelForUsageBasis,
} from "../schemas/material-credit.ts";
export type {
MaterialSourceType,
MaterialCreditStatus,
UsageBasisType,
} from "../schemas/material-credit.ts";
export interface MaterialCreditRecord {
file: string;
lang?: string;
credit: MaterialCredit;
}
export type RawMaterialCreditMap = Record;
function rawToString(raw: string | { default?: string }): string {
return typeof raw === "string" ? raw : (raw.default ?? "");
}
export function langFromCreditPath(path: string): string | undefined {
const match = path.match(/\/src\/content\/[^/]+\/([^/]+)\//);
if (!match) return undefined;
const segment = match[1];
// RFC-0220: language-agnostic asset folders (surface/assets/, shared/assets/)
// carry no lang segment; the sidecar itself is the canonical record.
return segment === "assets" || segment === "media" ? undefined : segment;
}
export function parseMaterialCreditMap(rawMap: RawMaterialCreditMap): MaterialCreditRecord[] {
const records: MaterialCreditRecord[] = [];
for (const [file, raw] of Object.entries(rawMap)) {
const parsed = parseYaml(rawToString(raw));
const credit = materialCreditSchema.parse(parsed);
records.push({ file, lang: langFromCreditPath(file), credit });
}
return records;
}
export function findMaterialCredit(
records: MaterialCreditRecord[],
target: MaterialTarget,
lang: string,
defaultLang: string,
): MaterialCredit | null {
const candidates = lang === defaultLang ? [lang, undefined] : [lang, defaultLang, undefined];
for (const candidateLang of candidates) {
const key = materialTargetKey(target, candidateLang);
const record = records.find((item) => {
const recordLang = item.credit.target.lang ?? item.lang;
return materialTargetKey(item.credit.target, recordLang) === key;
});
if (record) return record.credit;
}
return null;
}
export function creditByTarget(
rawMap: RawMaterialCreditMap,
target: MaterialTarget,
lang: string,
defaultLang: string,
): MaterialCredit | null {
return findMaterialCredit(parseMaterialCreditMap(rawMap), target, lang, defaultLang);
}
export function schemaTypeForMaterial(
kind: MaterialKind,
): "ImageObject" | "VideoObject" | "CreativeWork" {
if (kind === "image") return "ImageObject";
if (kind === "video") return "VideoObject";
return "CreativeWork";
}
/**
* RFC-0227: stable @id for a material credit node, derived from the content URL.
* Used by both the disclosure