feat: integrate n+1 target highlighting

- Merge feature branch changes for n+1 target-only highlight flow

- Extend merged token model and token-merger to mark exactly-one unknown targets

- Thread n+1 candidate metadata through tokenizer and config systems

- Update subtitle renderer/state to route configured colors and new token class

- Resolve merge conflicts in core service tests, including subtitle and subsync behavior
This commit is contained in:
2026-02-15 02:36:48 -08:00
parent 88099e2ffa
commit 3a27c026b6
16 changed files with 494 additions and 66 deletions

View File

@@ -36,6 +36,15 @@ function asBoolean(value: unknown): boolean | undefined {
return typeof value === "boolean" ? value : undefined;
}
const hexColorPattern =
/^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;
function asColor(value: unknown): string | undefined {
if (typeof value !== "string") return undefined;
const text = value.trim();
return hexColorPattern.test(text) ? text : undefined;
}
export class ConfigService {
private readonly configDir: string;
private readonly configFileJsonc: string;
@@ -751,6 +760,34 @@ export class ConfigService {
resolved.ankiConnect.nPlusOne.decks = [];
}
const nPlusOneHighlightColor = asColor(nPlusOneConfig.nPlusOne);
if (nPlusOneHighlightColor !== undefined) {
resolved.ankiConnect.nPlusOne.nPlusOne = nPlusOneHighlightColor;
} else if (nPlusOneConfig.nPlusOne !== undefined) {
warn(
"ankiConnect.nPlusOne.nPlusOne",
nPlusOneConfig.nPlusOne,
resolved.ankiConnect.nPlusOne.nPlusOne,
"Expected a hex color value.",
);
resolved.ankiConnect.nPlusOne.nPlusOne =
DEFAULT_CONFIG.ankiConnect.nPlusOne.nPlusOne;
}
const nPlusOneKnownWordColor = asColor(nPlusOneConfig.knownWord);
if (nPlusOneKnownWordColor !== undefined) {
resolved.ankiConnect.nPlusOne.knownWord = nPlusOneKnownWordColor;
} else if (nPlusOneConfig.knownWord !== undefined) {
warn(
"ankiConnect.nPlusOne.knownWord",
nPlusOneConfig.knownWord,
resolved.ankiConnect.nPlusOne.knownWord,
"Expected a hex color value.",
);
resolved.ankiConnect.nPlusOne.knownWord =
DEFAULT_CONFIG.ankiConnect.nPlusOne.knownWord;
}
if (
resolved.ankiConnect.isKiku.fieldGrouping !== "auto" &&
resolved.ankiConnect.isKiku.fieldGrouping !== "manual" &&