Reduction order and tree traversal
The policy implementation follows.
ts
export function defineContentProjection(options: ContentProjectionOptions) {
const counts = [...options.counts],
lengths = [...options.lengths];
const render = options.render.bind(options),
fallback = options.fallback.bind(options);
function shorten(value: unknown, length: number, count: number): unknown {
// The ellipsis is explicit so a reader can tell truncated text from original text.
if (typeof value === 'string')
return value.length <= length ? value : value.slice(0, length) + '…';
if (Array.isArray(value))
return value.slice(0, count).map((child) => shorten(child, length, count));
if (value && typeof value === 'object')
return Object.fromEntries(
Object.entries(value).map(([key, child]) => [key, shorten(child, length, count)]),
);
return value;
}
function project({
value,
maxChars: limit,
}: {
value: unknown;
maxChars: number;
}): ContentProjection {
if (!Number.isSafeInteger(limit) || limit < 0)
throw new TypeError('Invalid projection maxChars');
const original = JSON.stringify(value);
if (original.length <= limit) return { text: original, truncated: false };
for (const count of counts)
for (const length of lengths) {
const text = synchronous(
render(shorten(value, length, count)),
'Projection renderers must be synchronous',
);
if (text.length <= limit) return { text, truncated: true };
}
const text = synchronous(fallback(limit), 'Projection fallback must be synchronous');
validateContextBudget(text.length, limit);
return { text, truncated: true };
}
return Object.freeze({ project });
}
export function projectContinuousExcerpt(
body: string,
limit: number,
render: (length: number) => string,
): ContentProjection | undefined {
validateContextBudget(0, limit);
const excerpt = (length: number) =>
synchronous(render(length), 'Excerpt renderers must be synchronous');
if (excerpt(0).length > limit) return;
let low = 0,
high = body.length;
while (low < high) {
const mid = Math.ceil((low + high) / 2);
if (excerpt(mid).length <= limit) low = mid;
else high = mid - 1;
}
if (low > 0 && low < body.length && /[\uD800-\uDBFF]/.test(body[low - 1])) low--;
const text = excerpt(low);
validateContextBudget(text.length, limit);
return { text, truncated: true };
}
export function defineTreeProjection<Context>(options: TreeProjectionOptions<Context>) {
const { maxDepth } = options,
omit = options.omit?.bind(options),
visitRecord = options.visitRecord?.bind(options);
if (!Number.isSafeInteger(maxDepth) || maxDepth < 0) throw new TypeError('Invalid tree maxDepth');
function visit(value: unknown, depth: number, context: Context): unknown {
if (depth > maxDepth) return undefined;
if (
omit &&
booleanResult(
omit(value, depth, context),
'Tree omit predicates must return boolean synchronously',
)
)
return undefined;
if (Array.isArray(value))
return value
.map((item) => visit(item, depth + 1, context))
.filter((item) => item !== undefined);
if (value === null || typeof value !== 'object') return value;
const record = value as Record<string, unknown>;
synchronous(visitRecord?.(record, depth, context), 'Tree visitors must be synchronous');
return Object.fromEntries(
Object.entries(record).flatMap(([key, item]) => {
const next = visit(item, depth + 1, context);
return next === undefined ? [] : [[key, next]];
}),
);
}
const project = (value: unknown, context: Context): unknown => visit(value, 0, context);
return Object.freeze({ visit: project });
}