Moves ~25 components, utils (search, filename, groupDocuments, documentStatusLabel, validateFile), bulkSelection store, and TranscriptionSection sub-component. Fixes broken relative imports. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
47 lines
1.8 KiB
TypeScript
47 lines
1.8 KiB
TypeScript
export type TextSegment = { text: string; highlight: boolean };
|
|
|
|
export type MatchOffset = { start: number; length: number };
|
|
|
|
/**
|
|
* Converts a flat string and a list of character-level highlight offsets into
|
|
* an array of text segments that can be rendered without {@html}.
|
|
*
|
|
* Offsets are sorted and merged (overlapping spans become the longest enclosing
|
|
* span) before processing. Out-of-bounds offsets are clamped or dropped.
|
|
*
|
|
* @param text The display text (no delimiter characters).
|
|
* @param offsets Character offsets produced by the backend (Java char positions,
|
|
* compatible with JavaScript String indexing).
|
|
*/
|
|
export function applyOffsets(text: string, offsets: MatchOffset[]): TextSegment[] {
|
|
if (!offsets.length) return [{ text, highlight: false }];
|
|
|
|
// Sort by start position and merge overlapping / adjacent spans
|
|
const sorted = [...offsets].sort((a, b) => a.start - b.start);
|
|
const merged: { start: number; end: number }[] = [];
|
|
for (const { start, length } of sorted) {
|
|
const end = start + length;
|
|
if (end <= 0 || start >= text.length) continue; // completely out of bounds
|
|
const clampedStart = Math.max(0, start);
|
|
const clampedEnd = Math.min(text.length, end);
|
|
const last = merged[merged.length - 1];
|
|
if (!last || clampedStart > last.end) {
|
|
merged.push({ start: clampedStart, end: clampedEnd });
|
|
} else {
|
|
last.end = Math.max(last.end, clampedEnd);
|
|
}
|
|
}
|
|
|
|
if (!merged.length) return [{ text, highlight: false }];
|
|
|
|
const segments: TextSegment[] = [];
|
|
let pos = 0;
|
|
for (const { start, end } of merged) {
|
|
if (pos < start) segments.push({ text: text.slice(pos, start), highlight: false });
|
|
segments.push({ text: text.slice(start, end), highlight: true });
|
|
pos = end;
|
|
}
|
|
if (pos < text.length) segments.push({ text: text.slice(pos), highlight: false });
|
|
return segments;
|
|
}
|