/*
Framework-agnostic deep walk that applies a string-substitution function to every
string leaf of an arbitrary data structure, leaving non-string values unchanged (RFC-0138).
- Do not parse or resolve {collection.file.field} references — that is the caller's
substitute function (RFC-0045 / RFC-0050 resolvers).
- Do not mutate the input — return a new structure.
- RFC-0138: extracted the recursive prop-walk so block-prop reference substitution is
shared by the astro resolver and the page handler, and unit-testable without astro.
- ADR-0105: pass non-plain objects through untouched — Object.entries() on a Date/class
instance yields [], so rebuilding silently replaced it with {} and shipped
datetime="[object Object]" + "Invalid Date" to production.
*/
/**
* Recursively substitutes string leaves of a data structure using `substituteString`.
*
* - strings → `await substituteString(value)`
* - arrays / plain objects → recurse
* - everything else (number, boolean, null, undefined) → identity
*
* The input is not mutated; a new structure is returned.
*/
export async function substituteRefsDeep(
data: unknown,
substituteString: (value: string) => Promise,
): Promise {
if (typeof data === "string") {
return substituteString(data);
}
if (Array.isArray(data)) {
return Promise.all(data.map((item) => substituteRefsDeep(item, substituteString)));
}
// Only plain objects are rebuilt. Class instances (Date, URL, custom classes)
// must pass through untouched: Object.entries() on a Date yields [], so
// rebuilding would silently replace it with {} (ADR-0105). A prototype check
// (not toString-tag) is required — class instances without Symbol.toStringTag
// still report "[object Object]".
if (data !== null && typeof data === "object") {
const proto = Object.getPrototypeOf(data);
if (proto !== Object.prototype && proto !== null) {
return data;
}
const result: Record = {};
for (const [key, value] of Object.entries(data as Record)) {
result[key] = await substituteRefsDeep(value, substituteString);
}
return result;
}
return data;
}