Skip to content
English

缩减顺序与树遍历 ​

策略的具体实现如下。

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 });
}

接口参考、使用示例与开发指南。