A tiny string pipeline that keeps highlights stable while truncating

Hey everyone, I’m wiring up a search results list and I’m trying to keep my string formatting predictable without mutating the same object in three different places. The failure mode I kept hitting was highlight ranges drifting after truncation, plus it’s easy to accidentally reuse mutated state between rows.

// Pure-ish pipeline: compute once, render many
export function formatSnippet(text, query, max = 140) {
  const q = query.trim();
  if (!q) return { head: text.slice(0, max), tail: text.length > max, hits: [] };

  const lower = text.toLowerCase();
  const ql = q.toLowerCase();
  const hits = [];

  for (let i = 0; (i = lower.indexOf(ql, i)) !== -1; i += ql.length) {
    hits.push([i, i + ql.length]);
  }

  // pick a window around first hit, but keep original indices for stable highlights
  const [s] = hits[0] ?? [0];
  const start = Math.max(0, s - Math.floor(max / 3));
  const end = Math.min(text.length, start + max);

  return {
    head: text.slice(start, end),
    offset: start,
    tail: end < text.length,
    hits // still in original coordinates
  };
}

export function toParts({ head, offset = 0, hits }) {
  // convert original hit ranges to local ranges without mutating hits
  const local = hits
    .map(([a, b]) => [a - offset, b - offset])
    .filter(([a, b]) => b > 0 && a < head.length);

  const parts = [];
  let cursor = 0;
  for (const [a, b] of local) {
    if (a > cursor) parts.push({ t: head.slice(cursor, a), hit: false });
    parts.push({ t: head.slice(Math.max(0, a), Math.min(head.length, b)), hit: true });
    cursor = Math.min(head.length, b);
  }
  if (cursor < head.length) parts.push({ t: head.slice(cursor), hit: false });
  return parts;
}

Neat part for me is the mutation strategy: I keep the “truth” (hit ranges) in original coordinates and only derive local ranges at render time, so truncation and wrapping don’t quietly corrupt shared state.