fix: address claude review feedback on overlay refactor

This commit is contained in:
2026-02-26 18:47:51 -08:00
parent fa0cb00f70
commit 337e3268f1
28 changed files with 95 additions and 197 deletions

View File

@@ -7,7 +7,7 @@ import type { MergedToken } from '../types';
import { PartOfSpeech } from '../types.js';
import {
alignTokensToSourceText,
buildInvisibleTokenHoverRanges,
buildSubtitleTokenHoverRanges,
computeWordClass,
normalizeSubtitle,
sanitizeSubtitleHoverTokenColor,
@@ -266,26 +266,26 @@ test('alignTokensToSourceText avoids duplicate tail when later token surface doe
);
});
test('buildInvisibleTokenHoverRanges tracks token offsets across text separators', () => {
test('buildSubtitleTokenHoverRanges tracks token offsets across text separators', () => {
const tokens = [
createToken({ surface: 'キリキリと' }),
createToken({ surface: 'かかってこい' }),
];
const ranges = buildInvisibleTokenHoverRanges(tokens, 'キリキリと\nかかってこい');
const ranges = buildSubtitleTokenHoverRanges(tokens, 'キリキリと\nかかってこい');
assert.deepEqual(ranges, [
{ start: 0, end: 5, tokenIndex: 0 },
{ start: 6, end: 12, tokenIndex: 1 },
]);
});
test('buildInvisibleTokenHoverRanges ignores unmatched token surfaces', () => {
test('buildSubtitleTokenHoverRanges ignores unmatched token surfaces', () => {
const tokens = [
createToken({ surface: '君たちが潰した拠点に' }),
createToken({ surface: '教団の主力は1人もいない' }),
];
const ranges = buildInvisibleTokenHoverRanges(tokens, '君たちが潰した拠点に\n教団の主力は人もいない');
const ranges = buildSubtitleTokenHoverRanges(tokens, '君たちが潰した拠点に\n教団の主力は人もいない');
assert.deepEqual(ranges, [{ start: 0, end: 10, tokenIndex: 0 }]);
});

View File

@@ -9,7 +9,7 @@ type FrequencyRenderSettings = {
bandedColors: [string, string, string, string, string];
};
export type InvisibleTokenHoverRange = {
export type SubtitleTokenHoverRange = {
start: number;
end: number;
tokenIndex: number;
@@ -37,6 +37,8 @@ export function normalizeSubtitle(text: string, trim = true, collapseLineBreaks
}
const HEX_COLOR_PATTERN = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;
const SAFE_CSS_COLOR_PATTERN =
/^(?:#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|(?:rgba?|hsla?)\([^)]*\)|var\([^)]*\)|[a-zA-Z]+)$/;
function sanitizeHexColor(value: unknown, fallback: string): string {
return typeof value === 'string' && HEX_COLOR_PATTERN.test(value.trim())
@@ -58,7 +60,9 @@ function sanitizeSubtitleHoverTokenBackgroundColor(value: unknown): string {
return 'rgba(54, 58, 79, 0.84)';
}
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : 'rgba(54, 58, 79, 0.84)';
return trimmed.length > 0 && SAFE_CSS_COLOR_PATTERN.test(trimmed)
? trimmed
: 'rgba(54, 58, 79, 0.84)';
}
const DEFAULT_FREQUENCY_RENDER_SETTINGS: FrequencyRenderSettings = {
@@ -293,16 +297,16 @@ export function alignTokensToSourceText(
return segments;
}
export function buildInvisibleTokenHoverRanges(
export function buildSubtitleTokenHoverRanges(
tokens: MergedToken[],
sourceText: string,
): InvisibleTokenHoverRange[] {
): SubtitleTokenHoverRange[] {
if (tokens.length === 0 || sourceText.length === 0) {
return [];
}
const segments = alignTokensToSourceText(tokens, sourceText);
const ranges: InvisibleTokenHoverRange[] = [];
const ranges: SubtitleTokenHoverRange[] = [];
let cursor = 0;
for (const segment of segments) {