mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-03-06 19:57:26 -08:00
feat(subtitles): highlight character-name tokens
This commit is contained in:
@@ -0,0 +1,39 @@
|
|||||||
|
---
|
||||||
|
id: TASK-97
|
||||||
|
title: Add configurable character-name token highlighting
|
||||||
|
status: Done
|
||||||
|
assignee: []
|
||||||
|
created_date: '2026-03-06 10:15'
|
||||||
|
updated_date: '2026-03-06 10:15'
|
||||||
|
labels:
|
||||||
|
- subtitle
|
||||||
|
- dictionary
|
||||||
|
- renderer
|
||||||
|
dependencies: []
|
||||||
|
references:
|
||||||
|
- /home/sudacode/projects/japanese/SubMiner/src/core/services/tokenizer.ts
|
||||||
|
- >-
|
||||||
|
/home/sudacode/projects/japanese/SubMiner/src/core/services/tokenizer/yomitan-parser-runtime.ts
|
||||||
|
- /home/sudacode/projects/japanese/SubMiner/src/renderer/subtitle-render.ts
|
||||||
|
priority: medium
|
||||||
|
---
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||||
|
Color subtitle tokens that match entries from the SubMiner character dictionary, with a configurable default color and a config toggle that disables both rendering and name-match detection work.
|
||||||
|
<!-- SECTION:DESCRIPTION:END -->
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
<!-- AC:BEGIN -->
|
||||||
|
- [x] #1 Tokens matched from the SubMiner character dictionary receive dedicated renderer styling.
|
||||||
|
- [x] #2 `subtitleStyle.nameMatchEnabled` disables name-match detection work when false.
|
||||||
|
- [x] #3 `subtitleStyle.nameMatchColor` overrides the default `#f5bde6`.
|
||||||
|
- [x] #4 Regression coverage verifies config parsing, tokenizer propagation, scanner gating, and renderer class/CSS behavior.
|
||||||
|
<!-- AC:END -->
|
||||||
|
|
||||||
|
## Final Summary
|
||||||
|
|
||||||
|
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
||||||
|
Added configurable character-name token highlighting with default color `#f5bde6` and config gate `subtitleStyle.nameMatchEnabled`. When enabled, left-to-right Yomitan scanning tags tokens whose winning dictionary entry comes from the SubMiner character dictionary; when disabled, the tokenizer skips that metadata work and the renderer suppresses name-match styling. Added focused regression tests for config parsing, main-deps wiring, Yomitan scan gating, token propagation, renderer classes, and CSS behavior.
|
||||||
|
<!-- SECTION:FINAL_SUMMARY:END -->
|
||||||
@@ -242,6 +242,49 @@ test('parses subtitleStyle.hoverTokenColor and warns on invalid values', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('parses subtitleStyle.nameMatchColor and warns on invalid values', () => {
|
||||||
|
const validDir = makeTempDir();
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(validDir, 'config.jsonc'),
|
||||||
|
`{
|
||||||
|
"subtitleStyle": {
|
||||||
|
"nameMatchColor": "#eed49f"
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
'utf-8',
|
||||||
|
);
|
||||||
|
|
||||||
|
const validService = new ConfigService(validDir);
|
||||||
|
assert.equal(
|
||||||
|
((validService.getConfig().subtitleStyle as unknown as Record<string, unknown>).nameMatchColor ??
|
||||||
|
null) as string | null,
|
||||||
|
'#eed49f',
|
||||||
|
);
|
||||||
|
|
||||||
|
const invalidDir = makeTempDir();
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(invalidDir, 'config.jsonc'),
|
||||||
|
`{
|
||||||
|
"subtitleStyle": {
|
||||||
|
"nameMatchColor": "pink"
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
'utf-8',
|
||||||
|
);
|
||||||
|
|
||||||
|
const invalidService = new ConfigService(invalidDir);
|
||||||
|
assert.equal(
|
||||||
|
((invalidService.getConfig().subtitleStyle as unknown as Record<string, unknown>)
|
||||||
|
.nameMatchColor ?? null) as string | null,
|
||||||
|
'#f5bde6',
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
invalidService
|
||||||
|
.getWarnings()
|
||||||
|
.some((warning) => warning.path === 'subtitleStyle.nameMatchColor'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test('parses subtitleStyle.hoverTokenBackgroundColor and warns on invalid values', () => {
|
test('parses subtitleStyle.hoverTokenBackgroundColor and warns on invalid values', () => {
|
||||||
const validDir = makeTempDir();
|
const validDir = makeTempDir();
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
@@ -280,6 +323,44 @@ test('parses subtitleStyle.hoverTokenBackgroundColor and warns on invalid values
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('parses subtitleStyle.nameMatchEnabled and warns on invalid values', () => {
|
||||||
|
const validDir = makeTempDir();
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(validDir, 'config.jsonc'),
|
||||||
|
`{
|
||||||
|
"subtitleStyle": {
|
||||||
|
"nameMatchEnabled": false
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
'utf-8',
|
||||||
|
);
|
||||||
|
|
||||||
|
const validService = new ConfigService(validDir);
|
||||||
|
assert.equal(validService.getConfig().subtitleStyle.nameMatchEnabled, false);
|
||||||
|
|
||||||
|
const invalidDir = makeTempDir();
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(invalidDir, 'config.jsonc'),
|
||||||
|
`{
|
||||||
|
"subtitleStyle": {
|
||||||
|
"nameMatchEnabled": "no"
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
'utf-8',
|
||||||
|
);
|
||||||
|
|
||||||
|
const invalidService = new ConfigService(invalidDir);
|
||||||
|
assert.equal(
|
||||||
|
invalidService.getConfig().subtitleStyle.nameMatchEnabled,
|
||||||
|
DEFAULT_CONFIG.subtitleStyle.nameMatchEnabled,
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
invalidService
|
||||||
|
.getWarnings()
|
||||||
|
.some((warning) => warning.path === 'subtitleStyle.nameMatchEnabled'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test('parses anilist.enabled and warns for invalid value', () => {
|
test('parses anilist.enabled and warns for invalid value', () => {
|
||||||
const dir = makeTempDir();
|
const dir = makeTempDir();
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ export const SUBTITLE_DEFAULT_CONFIG: Pick<ResolvedConfig, 'subtitleStyle'> = {
|
|||||||
autoPauseVideoOnYomitanPopup: false,
|
autoPauseVideoOnYomitanPopup: false,
|
||||||
hoverTokenColor: '#f4dbd6',
|
hoverTokenColor: '#f4dbd6',
|
||||||
hoverTokenBackgroundColor: 'rgba(54, 58, 79, 0.84)',
|
hoverTokenBackgroundColor: 'rgba(54, 58, 79, 0.84)',
|
||||||
|
nameMatchEnabled: true,
|
||||||
|
nameMatchColor: '#f5bde6',
|
||||||
fontFamily: 'M PLUS 1 Medium, Source Han Sans JP, Noto Sans CJK JP',
|
fontFamily: 'M PLUS 1 Medium, Source Han Sans JP, Noto Sans CJK JP',
|
||||||
fontSize: 35,
|
fontSize: 35,
|
||||||
fontColor: '#cad3f5',
|
fontColor: '#cad3f5',
|
||||||
|
|||||||
@@ -47,6 +47,20 @@ export function buildSubtitleConfigOptionRegistry(
|
|||||||
defaultValue: defaultConfig.subtitleStyle.hoverTokenBackgroundColor,
|
defaultValue: defaultConfig.subtitleStyle.hoverTokenBackgroundColor,
|
||||||
description: 'CSS color used for hovered subtitle token background highlight in mpv.',
|
description: 'CSS color used for hovered subtitle token background highlight in mpv.',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'subtitleStyle.nameMatchEnabled',
|
||||||
|
kind: 'boolean',
|
||||||
|
defaultValue: defaultConfig.subtitleStyle.nameMatchEnabled,
|
||||||
|
description:
|
||||||
|
'Enable subtitle token coloring for matches from the SubMiner character dictionary.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'subtitleStyle.nameMatchColor',
|
||||||
|
kind: 'string',
|
||||||
|
defaultValue: defaultConfig.subtitleStyle.nameMatchColor,
|
||||||
|
description:
|
||||||
|
'Hex color used when a subtitle token matches an entry from the SubMiner character dictionary.',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'subtitleStyle.frequencyDictionary.enabled',
|
path: 'subtitleStyle.frequencyDictionary.enabled',
|
||||||
kind: 'boolean',
|
kind: 'boolean',
|
||||||
|
|||||||
@@ -105,6 +105,8 @@ export function applySubtitleDomainConfig(context: ResolveContext): void {
|
|||||||
const fallbackSubtitleStyleHoverTokenColor = resolved.subtitleStyle.hoverTokenColor;
|
const fallbackSubtitleStyleHoverTokenColor = resolved.subtitleStyle.hoverTokenColor;
|
||||||
const fallbackSubtitleStyleHoverTokenBackgroundColor =
|
const fallbackSubtitleStyleHoverTokenBackgroundColor =
|
||||||
resolved.subtitleStyle.hoverTokenBackgroundColor;
|
resolved.subtitleStyle.hoverTokenBackgroundColor;
|
||||||
|
const fallbackSubtitleStyleNameMatchEnabled = resolved.subtitleStyle.nameMatchEnabled;
|
||||||
|
const fallbackSubtitleStyleNameMatchColor = resolved.subtitleStyle.nameMatchColor;
|
||||||
const fallbackFrequencyDictionary = {
|
const fallbackFrequencyDictionary = {
|
||||||
...resolved.subtitleStyle.frequencyDictionary,
|
...resolved.subtitleStyle.frequencyDictionary,
|
||||||
};
|
};
|
||||||
@@ -228,6 +230,36 @@ export function applySubtitleDomainConfig(context: ResolveContext): void {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const nameMatchColor = asColor(
|
||||||
|
(src.subtitleStyle as { nameMatchColor?: unknown }).nameMatchColor,
|
||||||
|
);
|
||||||
|
const nameMatchEnabled = asBoolean(
|
||||||
|
(src.subtitleStyle as { nameMatchEnabled?: unknown }).nameMatchEnabled,
|
||||||
|
);
|
||||||
|
if (nameMatchEnabled !== undefined) {
|
||||||
|
resolved.subtitleStyle.nameMatchEnabled = nameMatchEnabled;
|
||||||
|
} else if ((src.subtitleStyle as { nameMatchEnabled?: unknown }).nameMatchEnabled !== undefined) {
|
||||||
|
resolved.subtitleStyle.nameMatchEnabled = fallbackSubtitleStyleNameMatchEnabled;
|
||||||
|
warn(
|
||||||
|
'subtitleStyle.nameMatchEnabled',
|
||||||
|
(src.subtitleStyle as { nameMatchEnabled?: unknown }).nameMatchEnabled,
|
||||||
|
resolved.subtitleStyle.nameMatchEnabled,
|
||||||
|
'Expected boolean.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nameMatchColor !== undefined) {
|
||||||
|
resolved.subtitleStyle.nameMatchColor = nameMatchColor;
|
||||||
|
} else if ((src.subtitleStyle as { nameMatchColor?: unknown }).nameMatchColor !== undefined) {
|
||||||
|
resolved.subtitleStyle.nameMatchColor = fallbackSubtitleStyleNameMatchColor;
|
||||||
|
warn(
|
||||||
|
'subtitleStyle.nameMatchColor',
|
||||||
|
(src.subtitleStyle as { nameMatchColor?: unknown }).nameMatchColor,
|
||||||
|
resolved.subtitleStyle.nameMatchColor,
|
||||||
|
'Expected hex color.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const frequencyDictionary = isObject(
|
const frequencyDictionary = isObject(
|
||||||
(src.subtitleStyle as { frequencyDictionary?: unknown }).frequencyDictionary,
|
(src.subtitleStyle as { frequencyDictionary?: unknown }).frequencyDictionary,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -66,6 +66,25 @@ test('subtitleStyle autoPauseVideoOnYomitanPopup falls back on invalid value', (
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('subtitleStyle nameMatchEnabled falls back on invalid value', () => {
|
||||||
|
const { context, warnings } = createResolveContext({
|
||||||
|
subtitleStyle: {
|
||||||
|
nameMatchEnabled: 'invalid' as unknown as boolean,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
applySubtitleDomainConfig(context);
|
||||||
|
|
||||||
|
assert.equal(context.resolved.subtitleStyle.nameMatchEnabled, true);
|
||||||
|
assert.ok(
|
||||||
|
warnings.some(
|
||||||
|
(warning) =>
|
||||||
|
warning.path === 'subtitleStyle.nameMatchEnabled' &&
|
||||||
|
warning.message === 'Expected boolean.',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test('subtitleStyle frequencyDictionary defaults to the teal fourth band color', () => {
|
test('subtitleStyle frequencyDictionary defaults to the teal fourth band color', () => {
|
||||||
const { context } = createResolveContext({});
|
const { context } = createResolveContext({});
|
||||||
|
|
||||||
@@ -80,6 +99,37 @@ test('subtitleStyle frequencyDictionary defaults to the teal fourth band color',
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('subtitleStyle nameMatchColor accepts valid values and warns on invalid', () => {
|
||||||
|
const valid = createResolveContext({
|
||||||
|
subtitleStyle: {
|
||||||
|
nameMatchColor: '#f5bde6',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
applySubtitleDomainConfig(valid.context);
|
||||||
|
assert.equal(
|
||||||
|
(valid.context.resolved.subtitleStyle as { nameMatchColor?: string }).nameMatchColor,
|
||||||
|
'#f5bde6',
|
||||||
|
);
|
||||||
|
|
||||||
|
const invalid = createResolveContext({
|
||||||
|
subtitleStyle: {
|
||||||
|
nameMatchColor: 'pink',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
applySubtitleDomainConfig(invalid.context);
|
||||||
|
assert.equal(
|
||||||
|
(invalid.context.resolved.subtitleStyle as { nameMatchColor?: string }).nameMatchColor,
|
||||||
|
'#f5bde6',
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
invalid.warnings.some(
|
||||||
|
(warning) =>
|
||||||
|
warning.path === 'subtitleStyle.nameMatchColor' &&
|
||||||
|
warning.message === 'Expected hex color.',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test('subtitleStyle frequencyDictionary.matchMode accepts valid values and warns on invalid', () => {
|
test('subtitleStyle frequencyDictionary.matchMode accepts valid values and warns on invalid', () => {
|
||||||
const valid = createResolveContext({
|
const valid = createResolveContext({
|
||||||
subtitleStyle: {
|
subtitleStyle: {
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ function makeDeps(overrides: Partial<AppReadyRuntimeDeps> = {}) {
|
|||||||
},
|
},
|
||||||
texthookerOnlyMode: false,
|
texthookerOnlyMode: false,
|
||||||
shouldAutoInitializeOverlayRuntimeFromConfig: () => true,
|
shouldAutoInitializeOverlayRuntimeFromConfig: () => true,
|
||||||
|
setVisibleOverlayVisible: (visible) => calls.push(`setVisibleOverlayVisible:${visible}`),
|
||||||
initializeOverlayRuntime: () => calls.push('initializeOverlayRuntime'),
|
initializeOverlayRuntime: () => calls.push('initializeOverlayRuntime'),
|
||||||
handleInitialArgs: () => calls.push('handleInitialArgs'),
|
handleInitialArgs: () => calls.push('handleInitialArgs'),
|
||||||
logDebug: (message) => calls.push(`debug:${message}`),
|
logDebug: (message) => calls.push(`debug:${message}`),
|
||||||
@@ -57,7 +58,11 @@ test('runAppReadyRuntime starts websocket in auto mode when plugin missing', asy
|
|||||||
});
|
});
|
||||||
await runAppReadyRuntime(deps);
|
await runAppReadyRuntime(deps);
|
||||||
assert.ok(calls.includes('startSubtitleWebsocket:9001'));
|
assert.ok(calls.includes('startSubtitleWebsocket:9001'));
|
||||||
|
assert.ok(calls.includes('setVisibleOverlayVisible:true'));
|
||||||
assert.ok(calls.includes('initializeOverlayRuntime'));
|
assert.ok(calls.includes('initializeOverlayRuntime'));
|
||||||
|
assert.ok(
|
||||||
|
calls.indexOf('setVisibleOverlayVisible:true') < calls.indexOf('initializeOverlayRuntime'),
|
||||||
|
);
|
||||||
assert.ok(calls.includes('startBackgroundWarmups'));
|
assert.ok(calls.includes('startBackgroundWarmups'));
|
||||||
assert.ok(
|
assert.ok(
|
||||||
calls.includes(
|
calls.includes(
|
||||||
|
|||||||
@@ -116,6 +116,7 @@ export interface AppReadyRuntimeDeps {
|
|||||||
startBackgroundWarmups: () => void;
|
startBackgroundWarmups: () => void;
|
||||||
texthookerOnlyMode: boolean;
|
texthookerOnlyMode: boolean;
|
||||||
shouldAutoInitializeOverlayRuntimeFromConfig: () => boolean;
|
shouldAutoInitializeOverlayRuntimeFromConfig: () => boolean;
|
||||||
|
setVisibleOverlayVisible: (visible: boolean) => void;
|
||||||
initializeOverlayRuntime: () => void;
|
initializeOverlayRuntime: () => void;
|
||||||
handleInitialArgs: () => void;
|
handleInitialArgs: () => void;
|
||||||
logDebug?: (message: string) => void;
|
logDebug?: (message: string) => void;
|
||||||
@@ -226,6 +227,7 @@ export async function runAppReadyRuntime(deps: AppReadyRuntimeDeps): Promise<voi
|
|||||||
if (deps.texthookerOnlyMode) {
|
if (deps.texthookerOnlyMode) {
|
||||||
deps.log('Texthooker-only mode enabled; skipping overlay window.');
|
deps.log('Texthooker-only mode enabled; skipping overlay window.');
|
||||||
} else if (deps.shouldAutoInitializeOverlayRuntimeFromConfig()) {
|
} else if (deps.shouldAutoInitializeOverlayRuntimeFromConfig()) {
|
||||||
|
deps.setVisibleOverlayVisible(true);
|
||||||
deps.initializeOverlayRuntime();
|
deps.initializeOverlayRuntime();
|
||||||
} else {
|
} else {
|
||||||
deps.log('Overlay runtime deferred: waiting for explicit overlay command.');
|
deps.log('Overlay runtime deferred: waiting for explicit overlay command.');
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ interface YomitanTokenInput {
|
|||||||
surface: string;
|
surface: string;
|
||||||
reading?: string;
|
reading?: string;
|
||||||
headword?: string;
|
headword?: string;
|
||||||
|
isNameMatch?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeDepsFromYomitanTokens(
|
function makeDepsFromYomitanTokens(
|
||||||
@@ -53,6 +54,7 @@ function makeDepsFromYomitanTokens(
|
|||||||
headword: token.headword ?? token.surface,
|
headword: token.headword ?? token.surface,
|
||||||
startPos,
|
startPos,
|
||||||
endPos,
|
endPos,
|
||||||
|
isNameMatch: token.isNameMatch ?? false,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -115,6 +117,20 @@ test('tokenizeSubtitle assigns JLPT level to parsed Yomitan tokens', async () =>
|
|||||||
assert.equal(result.tokens?.[0]?.jlptLevel, 'N5');
|
assert.equal(result.tokens?.[0]?.jlptLevel, 'N5');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('tokenizeSubtitle preserves Yomitan name-match metadata on tokens', async () => {
|
||||||
|
const result = await tokenizeSubtitle(
|
||||||
|
'アクアです',
|
||||||
|
makeDepsFromYomitanTokens([
|
||||||
|
{ surface: 'アクア', reading: 'あくあ', headword: 'アクア', isNameMatch: true },
|
||||||
|
{ surface: 'です', reading: 'です', headword: 'です' },
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(result.tokens?.length, 2);
|
||||||
|
assert.equal((result.tokens?.[0] as { isNameMatch?: boolean } | undefined)?.isNameMatch, true);
|
||||||
|
assert.equal((result.tokens?.[1] as { isNameMatch?: boolean } | undefined)?.isNameMatch, false);
|
||||||
|
});
|
||||||
|
|
||||||
test('tokenizeSubtitle caches JLPT lookups across repeated tokens', async () => {
|
test('tokenizeSubtitle caches JLPT lookups across repeated tokens', async () => {
|
||||||
let lookupCalls = 0;
|
let lookupCalls = 0;
|
||||||
const result = await tokenizeSubtitle(
|
const result = await tokenizeSubtitle(
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ export interface TokenizerServiceDeps {
|
|||||||
getJlptLevel: (text: string) => JlptLevel | null;
|
getJlptLevel: (text: string) => JlptLevel | null;
|
||||||
getNPlusOneEnabled?: () => boolean;
|
getNPlusOneEnabled?: () => boolean;
|
||||||
getJlptEnabled?: () => boolean;
|
getJlptEnabled?: () => boolean;
|
||||||
|
getNameMatchEnabled?: () => boolean;
|
||||||
getFrequencyDictionaryEnabled?: () => boolean;
|
getFrequencyDictionaryEnabled?: () => boolean;
|
||||||
getFrequencyDictionaryMatchMode?: () => FrequencyDictionaryMatchMode;
|
getFrequencyDictionaryMatchMode?: () => FrequencyDictionaryMatchMode;
|
||||||
getFrequencyRank?: FrequencyDictionaryLookup;
|
getFrequencyRank?: FrequencyDictionaryLookup;
|
||||||
@@ -73,6 +74,7 @@ export interface TokenizerDepsRuntimeOptions {
|
|||||||
getJlptLevel: (text: string) => JlptLevel | null;
|
getJlptLevel: (text: string) => JlptLevel | null;
|
||||||
getNPlusOneEnabled?: () => boolean;
|
getNPlusOneEnabled?: () => boolean;
|
||||||
getJlptEnabled?: () => boolean;
|
getJlptEnabled?: () => boolean;
|
||||||
|
getNameMatchEnabled?: () => boolean;
|
||||||
getFrequencyDictionaryEnabled?: () => boolean;
|
getFrequencyDictionaryEnabled?: () => boolean;
|
||||||
getFrequencyDictionaryMatchMode?: () => FrequencyDictionaryMatchMode;
|
getFrequencyDictionaryMatchMode?: () => FrequencyDictionaryMatchMode;
|
||||||
getFrequencyRank?: FrequencyDictionaryLookup;
|
getFrequencyRank?: FrequencyDictionaryLookup;
|
||||||
@@ -85,6 +87,7 @@ export interface TokenizerDepsRuntimeOptions {
|
|||||||
interface TokenizerAnnotationOptions {
|
interface TokenizerAnnotationOptions {
|
||||||
nPlusOneEnabled: boolean;
|
nPlusOneEnabled: boolean;
|
||||||
jlptEnabled: boolean;
|
jlptEnabled: boolean;
|
||||||
|
nameMatchEnabled: boolean;
|
||||||
frequencyEnabled: boolean;
|
frequencyEnabled: boolean;
|
||||||
frequencyMatchMode: FrequencyDictionaryMatchMode;
|
frequencyMatchMode: FrequencyDictionaryMatchMode;
|
||||||
minSentenceWordsForNPlusOne: number | undefined;
|
minSentenceWordsForNPlusOne: number | undefined;
|
||||||
@@ -190,6 +193,7 @@ export function createTokenizerDepsRuntime(
|
|||||||
getJlptLevel: options.getJlptLevel,
|
getJlptLevel: options.getJlptLevel,
|
||||||
getNPlusOneEnabled: options.getNPlusOneEnabled,
|
getNPlusOneEnabled: options.getNPlusOneEnabled,
|
||||||
getJlptEnabled: options.getJlptEnabled,
|
getJlptEnabled: options.getJlptEnabled,
|
||||||
|
getNameMatchEnabled: options.getNameMatchEnabled,
|
||||||
getFrequencyDictionaryEnabled: options.getFrequencyDictionaryEnabled,
|
getFrequencyDictionaryEnabled: options.getFrequencyDictionaryEnabled,
|
||||||
getFrequencyDictionaryMatchMode: options.getFrequencyDictionaryMatchMode ?? (() => 'headword'),
|
getFrequencyDictionaryMatchMode: options.getFrequencyDictionaryMatchMode ?? (() => 'headword'),
|
||||||
getFrequencyRank: options.getFrequencyRank,
|
getFrequencyRank: options.getFrequencyRank,
|
||||||
@@ -301,6 +305,7 @@ function normalizeSelectedYomitanTokens(tokens: MergedToken[]): MergedToken[] {
|
|||||||
isMerged: token.isMerged ?? true,
|
isMerged: token.isMerged ?? true,
|
||||||
isKnown: token.isKnown ?? false,
|
isKnown: token.isKnown ?? false,
|
||||||
isNPlusOneTarget: token.isNPlusOneTarget ?? false,
|
isNPlusOneTarget: token.isNPlusOneTarget ?? false,
|
||||||
|
isNameMatch: token.isNameMatch ?? false,
|
||||||
reading: normalizeYomitanMergedReading(token),
|
reading: normalizeYomitanMergedReading(token),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@@ -460,6 +465,7 @@ function getAnnotationOptions(deps: TokenizerServiceDeps): TokenizerAnnotationOp
|
|||||||
return {
|
return {
|
||||||
nPlusOneEnabled: deps.getNPlusOneEnabled?.() !== false,
|
nPlusOneEnabled: deps.getNPlusOneEnabled?.() !== false,
|
||||||
jlptEnabled: deps.getJlptEnabled?.() !== false,
|
jlptEnabled: deps.getJlptEnabled?.() !== false,
|
||||||
|
nameMatchEnabled: deps.getNameMatchEnabled?.() !== false,
|
||||||
frequencyEnabled: deps.getFrequencyDictionaryEnabled?.() !== false,
|
frequencyEnabled: deps.getFrequencyDictionaryEnabled?.() !== false,
|
||||||
frequencyMatchMode: deps.getFrequencyDictionaryMatchMode?.() ?? 'headword',
|
frequencyMatchMode: deps.getFrequencyDictionaryMatchMode?.() ?? 'headword',
|
||||||
minSentenceWordsForNPlusOne: deps.getMinSentenceWordsForNPlusOne?.(),
|
minSentenceWordsForNPlusOne: deps.getMinSentenceWordsForNPlusOne?.(),
|
||||||
@@ -473,7 +479,9 @@ async function parseWithYomitanInternalParser(
|
|||||||
deps: TokenizerServiceDeps,
|
deps: TokenizerServiceDeps,
|
||||||
options: TokenizerAnnotationOptions,
|
options: TokenizerAnnotationOptions,
|
||||||
): Promise<MergedToken[] | null> {
|
): Promise<MergedToken[] | null> {
|
||||||
const selectedTokens = await requestYomitanScanTokens(text, deps, logger);
|
const selectedTokens = await requestYomitanScanTokens(text, deps, logger, {
|
||||||
|
includeNameMatchMetadata: options.nameMatchEnabled,
|
||||||
|
});
|
||||||
if (!selectedTokens || selectedTokens.length === 0) {
|
if (!selectedTokens || selectedTokens.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -489,6 +497,7 @@ async function parseWithYomitanInternalParser(
|
|||||||
isMerged: true,
|
isMerged: true,
|
||||||
isKnown: false,
|
isKnown: false,
|
||||||
isNPlusOneTarget: false,
|
isNPlusOneTarget: false,
|
||||||
|
isNameMatch: token.isNameMatch ?? false,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import * as fs from 'fs';
|
|||||||
import * as os from 'os';
|
import * as os from 'os';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
|
import * as vm from 'node:vm';
|
||||||
import {
|
import {
|
||||||
getYomitanDictionaryInfo,
|
getYomitanDictionaryInfo,
|
||||||
importYomitanDictionaryFromZip,
|
importYomitanDictionaryFromZip,
|
||||||
@@ -39,6 +40,40 @@ function createDeps(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function runInjectedYomitanScript(
|
||||||
|
script: string,
|
||||||
|
handler: (action: string, params: unknown) => unknown,
|
||||||
|
): Promise<unknown> {
|
||||||
|
return await vm.runInNewContext(script, {
|
||||||
|
chrome: {
|
||||||
|
runtime: {
|
||||||
|
lastError: null,
|
||||||
|
sendMessage: (
|
||||||
|
payload: { action?: string; params?: unknown },
|
||||||
|
callback: (response: { result?: unknown; error?: { message?: string } }) => void,
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
callback({ result: handler(payload.action ?? '', payload.params) });
|
||||||
|
} catch (error) {
|
||||||
|
callback({ error: { message: (error as Error).message } });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Array,
|
||||||
|
Error,
|
||||||
|
JSON,
|
||||||
|
Map,
|
||||||
|
Math,
|
||||||
|
Number,
|
||||||
|
Object,
|
||||||
|
Promise,
|
||||||
|
RegExp,
|
||||||
|
Set,
|
||||||
|
String,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
test('syncYomitanDefaultAnkiServer updates default profile server when script reports update', async () => {
|
test('syncYomitanDefaultAnkiServer updates default profile server when script reports update', async () => {
|
||||||
let scriptValue = '';
|
let scriptValue = '';
|
||||||
const deps = createDeps(async (script) => {
|
const deps = createDeps(async (script) => {
|
||||||
@@ -450,6 +485,164 @@ test('requestYomitanScanTokens uses left-to-right termsFind scanning instead of
|
|||||||
assert.match(scannerScript ?? '', /deinflect:\s*true/);
|
assert.match(scannerScript ?? '', /deinflect:\s*true/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('requestYomitanScanTokens marks tokens backed by SubMiner character dictionary entries', async () => {
|
||||||
|
const deps = createDeps(async (script) => {
|
||||||
|
if (script.includes('optionsGetFull')) {
|
||||||
|
return {
|
||||||
|
profileCurrent: 0,
|
||||||
|
profiles: [
|
||||||
|
{
|
||||||
|
options: {
|
||||||
|
scanning: { length: 40 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
surface: 'アクア',
|
||||||
|
reading: 'あくあ',
|
||||||
|
headword: 'アクア',
|
||||||
|
startPos: 0,
|
||||||
|
endPos: 3,
|
||||||
|
isNameMatch: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
surface: 'です',
|
||||||
|
reading: 'です',
|
||||||
|
headword: 'です',
|
||||||
|
startPos: 3,
|
||||||
|
endPos: 5,
|
||||||
|
isNameMatch: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await requestYomitanScanTokens('アクアです', deps, {
|
||||||
|
error: () => undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(result?.length, 2);
|
||||||
|
assert.equal((result?.[0] as { isNameMatch?: boolean } | undefined)?.isNameMatch, true);
|
||||||
|
assert.equal((result?.[1] as { isNameMatch?: boolean } | undefined)?.isNameMatch, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('requestYomitanScanTokens skips name-match work when disabled', async () => {
|
||||||
|
let scannerScript = '';
|
||||||
|
const deps = createDeps(async (script) => {
|
||||||
|
if (script.includes('termsFind')) {
|
||||||
|
scannerScript = script;
|
||||||
|
}
|
||||||
|
if (script.includes('optionsGetFull')) {
|
||||||
|
return {
|
||||||
|
profileCurrent: 0,
|
||||||
|
profiles: [
|
||||||
|
{
|
||||||
|
options: {
|
||||||
|
scanning: { length: 40 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
surface: 'アクア',
|
||||||
|
reading: 'あくあ',
|
||||||
|
headword: 'アクア',
|
||||||
|
startPos: 0,
|
||||||
|
endPos: 3,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await requestYomitanScanTokens(
|
||||||
|
'アクア',
|
||||||
|
deps,
|
||||||
|
{ error: () => undefined },
|
||||||
|
{ includeNameMatchMetadata: false },
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(result?.length, 1);
|
||||||
|
assert.equal((result?.[0] as { isNameMatch?: boolean } | undefined)?.isNameMatch, undefined);
|
||||||
|
assert.match(scannerScript, /const includeNameMatchMetadata = false;/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('requestYomitanScanTokens marks grouped entries when SubMiner dictionary alias only exists on definitions', async () => {
|
||||||
|
let scannerScript = '';
|
||||||
|
const deps = createDeps(async (script) => {
|
||||||
|
if (script.includes('termsFind')) {
|
||||||
|
scannerScript = script;
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
if (script.includes('optionsGetFull')) {
|
||||||
|
return {
|
||||||
|
profileCurrent: 0,
|
||||||
|
profiles: [
|
||||||
|
{
|
||||||
|
options: {
|
||||||
|
scanning: { length: 40 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
await requestYomitanScanTokens(
|
||||||
|
'カズマ',
|
||||||
|
deps,
|
||||||
|
{ error: () => undefined },
|
||||||
|
{ includeNameMatchMetadata: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.match(scannerScript, /getPreferredHeadword/);
|
||||||
|
|
||||||
|
const result = await runInjectedYomitanScript(scannerScript, (action, params) => {
|
||||||
|
if (action === 'termsFind') {
|
||||||
|
const text = (params as { text?: string } | undefined)?.text;
|
||||||
|
if (text === 'カズマ') {
|
||||||
|
return {
|
||||||
|
originalTextLength: 3,
|
||||||
|
dictionaryEntries: [
|
||||||
|
{
|
||||||
|
dictionaryAlias: '',
|
||||||
|
headwords: [
|
||||||
|
{
|
||||||
|
term: 'カズマ',
|
||||||
|
reading: 'かずま',
|
||||||
|
sources: [{ originalText: 'カズマ', isPrimary: true, matchType: 'exact' }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
definitions: [
|
||||||
|
{ dictionary: 'JMdict', dictionaryAlias: 'JMdict' },
|
||||||
|
{
|
||||||
|
dictionary: 'SubMiner Character Dictionary (AniList 130298)',
|
||||||
|
dictionaryAlias: 'SubMiner Character Dictionary (AniList 130298)',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return { originalTextLength: 0, dictionaryEntries: [] };
|
||||||
|
}
|
||||||
|
throw new Error(`unexpected action: ${action}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(Array.isArray(result), true);
|
||||||
|
assert.equal((result as { length?: number } | null)?.length, 1);
|
||||||
|
assert.equal((result as Array<{ surface?: string }>)[0]?.surface, 'カズマ');
|
||||||
|
assert.equal((result as Array<{ headword?: string }>)[0]?.headword, 'カズマ');
|
||||||
|
assert.equal((result as Array<{ startPos?: number }>)[0]?.startPos, 0);
|
||||||
|
assert.equal((result as Array<{ endPos?: number }>)[0]?.endPos, 3);
|
||||||
|
assert.equal((result as Array<{ isNameMatch?: boolean }>)[0]?.isNameMatch, true);
|
||||||
|
});
|
||||||
|
|
||||||
test('getYomitanDictionaryInfo requests dictionary info via backend action', async () => {
|
test('getYomitanDictionaryInfo requests dictionary info via backend action', async () => {
|
||||||
let scriptValue = '';
|
let scriptValue = '';
|
||||||
const deps = createDeps(async (script) => {
|
const deps = createDeps(async (script) => {
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ export interface YomitanScanToken {
|
|||||||
headword: string;
|
headword: string;
|
||||||
startPos: number;
|
startPos: number;
|
||||||
endPos: number;
|
endPos: number;
|
||||||
|
isNameMatch?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface YomitanProfileMetadata {
|
interface YomitanProfileMetadata {
|
||||||
@@ -75,7 +76,8 @@ function isScanTokenArray(value: unknown): value is YomitanScanToken[] {
|
|||||||
typeof entry.reading === 'string' &&
|
typeof entry.reading === 'string' &&
|
||||||
typeof entry.headword === 'string' &&
|
typeof entry.headword === 'string' &&
|
||||||
typeof entry.startPos === 'number' &&
|
typeof entry.startPos === 'number' &&
|
||||||
typeof entry.endPos === 'number',
|
typeof entry.endPos === 'number' &&
|
||||||
|
(entry.isNameMatch === undefined || typeof entry.isNameMatch === 'boolean'),
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -772,24 +774,92 @@ const YOMITAN_SCANNING_HELPERS = String.raw`
|
|||||||
return segments;
|
return segments;
|
||||||
}
|
}
|
||||||
function getPreferredHeadword(dictionaryEntries, token) {
|
function getPreferredHeadword(dictionaryEntries, token) {
|
||||||
|
function appendDictionaryNames(target, value) {
|
||||||
|
if (!value || typeof value !== 'object') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const candidates = [
|
||||||
|
value.dictionary,
|
||||||
|
value.dictionaryName,
|
||||||
|
value.name,
|
||||||
|
value.title,
|
||||||
|
value.dictionaryTitle,
|
||||||
|
value.dictionaryAlias
|
||||||
|
];
|
||||||
|
for (const candidate of candidates) {
|
||||||
|
if (typeof candidate === 'string' && candidate.trim().length > 0) {
|
||||||
|
target.push(candidate.trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function getDictionaryEntryNames(entry) {
|
||||||
|
const names = [];
|
||||||
|
appendDictionaryNames(names, entry);
|
||||||
|
for (const definition of entry?.definitions || []) {
|
||||||
|
appendDictionaryNames(names, definition);
|
||||||
|
}
|
||||||
|
for (const frequency of entry?.frequencies || []) {
|
||||||
|
appendDictionaryNames(names, frequency);
|
||||||
|
}
|
||||||
|
for (const pronunciation of entry?.pronunciations || []) {
|
||||||
|
appendDictionaryNames(names, pronunciation);
|
||||||
|
}
|
||||||
|
return names;
|
||||||
|
}
|
||||||
|
function isNameDictionaryEntry(entry) {
|
||||||
|
if (!includeNameMatchMetadata || !entry || typeof entry !== 'object') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return getDictionaryEntryNames(entry).some((name) => name.startsWith("SubMiner Character Dictionary"));
|
||||||
|
}
|
||||||
|
function hasExactPrimarySource(headword, token) {
|
||||||
|
for (const src of headword.sources || []) {
|
||||||
|
if (src.originalText !== token) { continue; }
|
||||||
|
if (!src.isPrimary) { continue; }
|
||||||
|
if (src.matchType !== 'exact') { continue; }
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let matchedNameDictionary = false;
|
||||||
|
if (includeNameMatchMetadata) {
|
||||||
|
for (const dictionaryEntry of dictionaryEntries || []) {
|
||||||
|
if (!isNameDictionaryEntry(dictionaryEntry)) { continue; }
|
||||||
|
for (const headword of dictionaryEntry.headwords || []) {
|
||||||
|
if (!hasExactPrimarySource(headword, token)) { continue; }
|
||||||
|
matchedNameDictionary = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (matchedNameDictionary) { break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
for (const dictionaryEntry of dictionaryEntries || []) {
|
for (const dictionaryEntry of dictionaryEntries || []) {
|
||||||
for (const headword of dictionaryEntry.headwords || []) {
|
for (const headword of dictionaryEntry.headwords || []) {
|
||||||
const validSources = [];
|
if (!hasExactPrimarySource(headword, token)) { continue; }
|
||||||
for (const src of headword.sources || []) {
|
return {
|
||||||
if (src.originalText !== token) { continue; }
|
term: headword.term,
|
||||||
if (!src.isPrimary) { continue; }
|
reading: headword.reading,
|
||||||
if (src.matchType !== 'exact') { continue; }
|
isNameMatch: matchedNameDictionary || isNameDictionaryEntry(dictionaryEntry)
|
||||||
validSources.push(src);
|
};
|
||||||
}
|
|
||||||
if (validSources.length > 0) { return {term: headword.term, reading: headword.reading}; }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const fallback = dictionaryEntries?.[0]?.headwords?.[0];
|
const fallback = dictionaryEntries?.[0]?.headwords?.[0];
|
||||||
return fallback ? {term: fallback.term, reading: fallback.reading} : null;
|
return fallback
|
||||||
|
? {
|
||||||
|
term: fallback.term,
|
||||||
|
reading: fallback.reading,
|
||||||
|
isNameMatch: matchedNameDictionary || isNameDictionaryEntry(dictionaryEntries?.[0])
|
||||||
|
}
|
||||||
|
: null;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
function buildYomitanScanningScript(text: string, profileIndex: number, scanLength: number): string {
|
function buildYomitanScanningScript(
|
||||||
|
text: string,
|
||||||
|
profileIndex: number,
|
||||||
|
scanLength: number,
|
||||||
|
includeNameMatchMetadata: boolean,
|
||||||
|
): string {
|
||||||
return `
|
return `
|
||||||
(async () => {
|
(async () => {
|
||||||
const invoke = (action, params) =>
|
const invoke = (action, params) =>
|
||||||
@@ -811,6 +881,7 @@ function buildYomitanScanningScript(text: string, profileIndex: number, scanLeng
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
${YOMITAN_SCANNING_HELPERS}
|
${YOMITAN_SCANNING_HELPERS}
|
||||||
|
const includeNameMatchMetadata = ${includeNameMatchMetadata ? 'true' : 'false'};
|
||||||
const text = ${JSON.stringify(text)};
|
const text = ${JSON.stringify(text)};
|
||||||
const details = {matchType: "exact", deinflect: true};
|
const details = {matchType: "exact", deinflect: true};
|
||||||
const tokens = [];
|
const tokens = [];
|
||||||
@@ -834,6 +905,7 @@ ${YOMITAN_SCANNING_HELPERS}
|
|||||||
headword: preferredHeadword.term,
|
headword: preferredHeadword.term,
|
||||||
startPos: i,
|
startPos: i,
|
||||||
endPos: i + originalTextLength,
|
endPos: i + originalTextLength,
|
||||||
|
isNameMatch: includeNameMatchMetadata && preferredHeadword.isNameMatch === true,
|
||||||
});
|
});
|
||||||
i += originalTextLength;
|
i += originalTextLength;
|
||||||
continue;
|
continue;
|
||||||
@@ -944,6 +1016,9 @@ export async function requestYomitanScanTokens(
|
|||||||
text: string,
|
text: string,
|
||||||
deps: YomitanParserRuntimeDeps,
|
deps: YomitanParserRuntimeDeps,
|
||||||
logger: LoggerLike,
|
logger: LoggerLike,
|
||||||
|
options?: {
|
||||||
|
includeNameMatchMetadata?: boolean;
|
||||||
|
},
|
||||||
): Promise<YomitanScanToken[] | null> {
|
): Promise<YomitanScanToken[] | null> {
|
||||||
const yomitanExt = deps.getYomitanExt();
|
const yomitanExt = deps.getYomitanExt();
|
||||||
if (!text || !yomitanExt) {
|
if (!text || !yomitanExt) {
|
||||||
@@ -962,7 +1037,12 @@ export async function requestYomitanScanTokens(
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const rawResult = await parserWindow.webContents.executeJavaScript(
|
const rawResult = await parserWindow.webContents.executeJavaScript(
|
||||||
buildYomitanScanningScript(text, profileIndex, scanLength),
|
buildYomitanScanningScript(
|
||||||
|
text,
|
||||||
|
profileIndex,
|
||||||
|
scanLength,
|
||||||
|
options?.includeNameMatchMetadata === true,
|
||||||
|
),
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
if (isScanTokenArray(rawResult)) {
|
if (isScanTokenArray(rawResult)) {
|
||||||
|
|||||||
@@ -2184,6 +2184,7 @@ const { appReadyRuntimeRunner } = composeAppReadyRuntime({
|
|||||||
appState.backgroundMode
|
appState.backgroundMode
|
||||||
? false
|
? false
|
||||||
: configDerivedRuntime.shouldAutoInitializeOverlayRuntimeFromConfig(),
|
: configDerivedRuntime.shouldAutoInitializeOverlayRuntimeFromConfig(),
|
||||||
|
setVisibleOverlayVisible: (visible: boolean) => setVisibleOverlayVisible(visible),
|
||||||
initializeOverlayRuntime: () => initializeOverlayRuntime(),
|
initializeOverlayRuntime: () => initializeOverlayRuntime(),
|
||||||
handleInitialArgs: () => handleInitialArgs(),
|
handleInitialArgs: () => handleInitialArgs(),
|
||||||
shouldSkipHeavyStartup: () =>
|
shouldSkipHeavyStartup: () =>
|
||||||
@@ -2435,6 +2436,7 @@ const {
|
|||||||
'subtitle.annotation.jlpt',
|
'subtitle.annotation.jlpt',
|
||||||
getResolvedConfig().subtitleStyle.enableJlpt,
|
getResolvedConfig().subtitleStyle.enableJlpt,
|
||||||
),
|
),
|
||||||
|
getNameMatchEnabled: () => getResolvedConfig().subtitleStyle.nameMatchEnabled,
|
||||||
getFrequencyDictionaryEnabled: () =>
|
getFrequencyDictionaryEnabled: () =>
|
||||||
getRuntimeBooleanOption(
|
getRuntimeBooleanOption(
|
||||||
'subtitle.annotation.frequency',
|
'subtitle.annotation.frequency',
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ export interface AppReadyRuntimeDepsFactoryInput {
|
|||||||
startBackgroundWarmups: AppReadyRuntimeDeps['startBackgroundWarmups'];
|
startBackgroundWarmups: AppReadyRuntimeDeps['startBackgroundWarmups'];
|
||||||
texthookerOnlyMode: AppReadyRuntimeDeps['texthookerOnlyMode'];
|
texthookerOnlyMode: AppReadyRuntimeDeps['texthookerOnlyMode'];
|
||||||
shouldAutoInitializeOverlayRuntimeFromConfig: AppReadyRuntimeDeps['shouldAutoInitializeOverlayRuntimeFromConfig'];
|
shouldAutoInitializeOverlayRuntimeFromConfig: AppReadyRuntimeDeps['shouldAutoInitializeOverlayRuntimeFromConfig'];
|
||||||
|
setVisibleOverlayVisible: AppReadyRuntimeDeps['setVisibleOverlayVisible'];
|
||||||
initializeOverlayRuntime: AppReadyRuntimeDeps['initializeOverlayRuntime'];
|
initializeOverlayRuntime: AppReadyRuntimeDeps['initializeOverlayRuntime'];
|
||||||
handleInitialArgs: AppReadyRuntimeDeps['handleInitialArgs'];
|
handleInitialArgs: AppReadyRuntimeDeps['handleInitialArgs'];
|
||||||
onCriticalConfigErrors?: AppReadyRuntimeDeps['onCriticalConfigErrors'];
|
onCriticalConfigErrors?: AppReadyRuntimeDeps['onCriticalConfigErrors'];
|
||||||
@@ -99,6 +100,7 @@ export function createAppReadyRuntimeDeps(
|
|||||||
texthookerOnlyMode: params.texthookerOnlyMode,
|
texthookerOnlyMode: params.texthookerOnlyMode,
|
||||||
shouldAutoInitializeOverlayRuntimeFromConfig:
|
shouldAutoInitializeOverlayRuntimeFromConfig:
|
||||||
params.shouldAutoInitializeOverlayRuntimeFromConfig,
|
params.shouldAutoInitializeOverlayRuntimeFromConfig,
|
||||||
|
setVisibleOverlayVisible: params.setVisibleOverlayVisible,
|
||||||
initializeOverlayRuntime: params.initializeOverlayRuntime,
|
initializeOverlayRuntime: params.initializeOverlayRuntime,
|
||||||
handleInitialArgs: params.handleInitialArgs,
|
handleInitialArgs: params.handleInitialArgs,
|
||||||
onCriticalConfigErrors: params.onCriticalConfigErrors,
|
onCriticalConfigErrors: params.onCriticalConfigErrors,
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ test('app-ready main deps builder returns mapped app-ready runtime deps', async
|
|||||||
startBackgroundWarmups: () => calls.push('start-warmups'),
|
startBackgroundWarmups: () => calls.push('start-warmups'),
|
||||||
texthookerOnlyMode: false,
|
texthookerOnlyMode: false,
|
||||||
shouldAutoInitializeOverlayRuntimeFromConfig: () => true,
|
shouldAutoInitializeOverlayRuntimeFromConfig: () => true,
|
||||||
|
setVisibleOverlayVisible: () => calls.push('set-visible-overlay'),
|
||||||
initializeOverlayRuntime: () => calls.push('init-overlay'),
|
initializeOverlayRuntime: () => calls.push('init-overlay'),
|
||||||
handleInitialArgs: () => calls.push('handle-initial-args'),
|
handleInitialArgs: () => calls.push('handle-initial-args'),
|
||||||
onCriticalConfigErrors: () => {
|
onCriticalConfigErrors: () => {
|
||||||
@@ -58,6 +59,7 @@ test('app-ready main deps builder returns mapped app-ready runtime deps', async
|
|||||||
await onReady.loadYomitanExtension();
|
await onReady.loadYomitanExtension();
|
||||||
await onReady.prewarmSubtitleDictionaries?.();
|
await onReady.prewarmSubtitleDictionaries?.();
|
||||||
onReady.startBackgroundWarmups();
|
onReady.startBackgroundWarmups();
|
||||||
|
onReady.setVisibleOverlayVisible(true);
|
||||||
|
|
||||||
assert.deepEqual(calls, [
|
assert.deepEqual(calls, [
|
||||||
'load-subtitle-position',
|
'load-subtitle-position',
|
||||||
@@ -67,5 +69,6 @@ test('app-ready main deps builder returns mapped app-ready runtime deps', async
|
|||||||
'load-yomitan',
|
'load-yomitan',
|
||||||
'prewarm-dicts',
|
'prewarm-dicts',
|
||||||
'start-warmups',
|
'start-warmups',
|
||||||
|
'set-visible-overlay',
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export function createBuildAppReadyRuntimeMainDepsHandler(deps: AppReadyRuntimeD
|
|||||||
startBackgroundWarmups: deps.startBackgroundWarmups,
|
startBackgroundWarmups: deps.startBackgroundWarmups,
|
||||||
texthookerOnlyMode: deps.texthookerOnlyMode,
|
texthookerOnlyMode: deps.texthookerOnlyMode,
|
||||||
shouldAutoInitializeOverlayRuntimeFromConfig: deps.shouldAutoInitializeOverlayRuntimeFromConfig,
|
shouldAutoInitializeOverlayRuntimeFromConfig: deps.shouldAutoInitializeOverlayRuntimeFromConfig,
|
||||||
|
setVisibleOverlayVisible: deps.setVisibleOverlayVisible,
|
||||||
initializeOverlayRuntime: deps.initializeOverlayRuntime,
|
initializeOverlayRuntime: deps.initializeOverlayRuntime,
|
||||||
handleInitialArgs: deps.handleInitialArgs,
|
handleInitialArgs: deps.handleInitialArgs,
|
||||||
onCriticalConfigErrors: deps.onCriticalConfigErrors,
|
onCriticalConfigErrors: deps.onCriticalConfigErrors,
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ test('composeAppReadyRuntime returns reload/critical/app-ready handlers', () =>
|
|||||||
startBackgroundWarmups: () => {},
|
startBackgroundWarmups: () => {},
|
||||||
texthookerOnlyMode: false,
|
texthookerOnlyMode: false,
|
||||||
shouldAutoInitializeOverlayRuntimeFromConfig: () => false,
|
shouldAutoInitializeOverlayRuntimeFromConfig: () => false,
|
||||||
|
setVisibleOverlayVisible: () => {},
|
||||||
initializeOverlayRuntime: () => {},
|
initializeOverlayRuntime: () => {},
|
||||||
handleInitialArgs: () => {},
|
handleInitialArgs: () => {},
|
||||||
logDebug: () => {},
|
logDebug: () => {},
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export function resolveSubtitleStyleForRenderer(config: ResolvedConfig) {
|
|||||||
...config.subtitleStyle,
|
...config.subtitleStyle,
|
||||||
nPlusOneColor: config.ankiConnect.nPlusOne.nPlusOne,
|
nPlusOneColor: config.ankiConnect.nPlusOne.nPlusOne,
|
||||||
knownWordColor: config.ankiConnect.nPlusOne.knownWord,
|
knownWordColor: config.ankiConnect.nPlusOne.knownWord,
|
||||||
|
nameMatchColor: config.subtitleStyle.nameMatchColor,
|
||||||
enableJlpt: config.subtitleStyle.enableJlpt,
|
enableJlpt: config.subtitleStyle.enableJlpt,
|
||||||
frequencyDictionary: config.subtitleStyle.frequencyDictionary,
|
frequencyDictionary: config.subtitleStyle.frequencyDictionary,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ test('tokenizer deps builder records known-word lookups and maps readers', () =>
|
|||||||
getMinSentenceWordsForNPlusOne: () => 3,
|
getMinSentenceWordsForNPlusOne: () => 3,
|
||||||
getJlptLevel: () => 'N2',
|
getJlptLevel: () => 'N2',
|
||||||
getJlptEnabled: () => true,
|
getJlptEnabled: () => true,
|
||||||
|
getNameMatchEnabled: () => false,
|
||||||
getFrequencyDictionaryEnabled: () => true,
|
getFrequencyDictionaryEnabled: () => true,
|
||||||
getFrequencyDictionaryMatchMode: () => 'surface',
|
getFrequencyDictionaryMatchMode: () => 'surface',
|
||||||
getFrequencyRank: () => 5,
|
getFrequencyRank: () => 5,
|
||||||
@@ -48,6 +49,7 @@ test('tokenizer deps builder records known-word lookups and maps readers', () =>
|
|||||||
deps.setYomitanParserInitPromise(null);
|
deps.setYomitanParserInitPromise(null);
|
||||||
assert.equal(deps.getNPlusOneEnabled?.(), true);
|
assert.equal(deps.getNPlusOneEnabled?.(), true);
|
||||||
assert.equal(deps.getMinSentenceWordsForNPlusOne?.(), 3);
|
assert.equal(deps.getMinSentenceWordsForNPlusOne?.(), 3);
|
||||||
|
assert.equal(deps.getNameMatchEnabled?.(), false);
|
||||||
assert.equal(deps.getFrequencyDictionaryMatchMode?.(), 'surface');
|
assert.equal(deps.getFrequencyDictionaryMatchMode?.(), 'surface');
|
||||||
assert.deepEqual(calls, ['lookup:true', 'lookup:false', 'set-window', 'set-ready', 'set-init']);
|
assert.deepEqual(calls, ['lookup:true', 'lookup:false', 'set-window', 'set-ready', 'set-init']);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { TokenizerDepsRuntimeOptions } from '../../core/services/tokenizer'
|
|||||||
|
|
||||||
type TokenizerMainDeps = TokenizerDepsRuntimeOptions & {
|
type TokenizerMainDeps = TokenizerDepsRuntimeOptions & {
|
||||||
getJlptEnabled: NonNullable<TokenizerDepsRuntimeOptions['getJlptEnabled']>;
|
getJlptEnabled: NonNullable<TokenizerDepsRuntimeOptions['getJlptEnabled']>;
|
||||||
|
getNameMatchEnabled?: NonNullable<TokenizerDepsRuntimeOptions['getNameMatchEnabled']>;
|
||||||
getFrequencyDictionaryEnabled: NonNullable<
|
getFrequencyDictionaryEnabled: NonNullable<
|
||||||
TokenizerDepsRuntimeOptions['getFrequencyDictionaryEnabled']
|
TokenizerDepsRuntimeOptions['getFrequencyDictionaryEnabled']
|
||||||
>;
|
>;
|
||||||
@@ -43,6 +44,11 @@ export function createBuildTokenizerDepsMainHandler(deps: TokenizerMainDeps) {
|
|||||||
getMinSentenceWordsForNPlusOne: () => deps.getMinSentenceWordsForNPlusOne(),
|
getMinSentenceWordsForNPlusOne: () => deps.getMinSentenceWordsForNPlusOne(),
|
||||||
getJlptLevel: (text: string) => deps.getJlptLevel(text),
|
getJlptLevel: (text: string) => deps.getJlptLevel(text),
|
||||||
getJlptEnabled: () => deps.getJlptEnabled(),
|
getJlptEnabled: () => deps.getJlptEnabled(),
|
||||||
|
...(deps.getNameMatchEnabled
|
||||||
|
? {
|
||||||
|
getNameMatchEnabled: () => deps.getNameMatchEnabled!(),
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
getFrequencyDictionaryEnabled: () => deps.getFrequencyDictionaryEnabled(),
|
getFrequencyDictionaryEnabled: () => deps.getFrequencyDictionaryEnabled(),
|
||||||
getFrequencyDictionaryMatchMode: () => deps.getFrequencyDictionaryMatchMode(),
|
getFrequencyDictionaryMatchMode: () => deps.getFrequencyDictionaryMatchMode(),
|
||||||
getFrequencyRank: (text: string) => deps.getFrequencyRank(text),
|
getFrequencyRank: (text: string) => deps.getFrequencyRank(text),
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ const HEX_COLOR_RE = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA
|
|||||||
const FALLBACK_COLORS = {
|
const FALLBACK_COLORS = {
|
||||||
knownWordColor: '#a6da95',
|
knownWordColor: '#a6da95',
|
||||||
nPlusOneColor: '#c6a0f6',
|
nPlusOneColor: '#c6a0f6',
|
||||||
|
nameMatchColor: '#f5bde6',
|
||||||
jlptN1Color: '#ed8796',
|
jlptN1Color: '#ed8796',
|
||||||
jlptN2Color: '#f5a97f',
|
jlptN2Color: '#f5a97f',
|
||||||
jlptN3Color: '#f9e2af',
|
jlptN3Color: '#f9e2af',
|
||||||
@@ -207,6 +208,7 @@ function buildBindingSections(keybindings: Keybinding[]): SessionHelpSection[] {
|
|||||||
function buildColorSection(style: {
|
function buildColorSection(style: {
|
||||||
knownWordColor?: unknown;
|
knownWordColor?: unknown;
|
||||||
nPlusOneColor?: unknown;
|
nPlusOneColor?: unknown;
|
||||||
|
nameMatchColor?: unknown;
|
||||||
jlptColors?: {
|
jlptColors?: {
|
||||||
N1?: unknown;
|
N1?: unknown;
|
||||||
N2?: unknown;
|
N2?: unknown;
|
||||||
@@ -228,6 +230,11 @@ function buildColorSection(style: {
|
|||||||
action: normalizeColor(style.nPlusOneColor, FALLBACK_COLORS.nPlusOneColor),
|
action: normalizeColor(style.nPlusOneColor, FALLBACK_COLORS.nPlusOneColor),
|
||||||
color: normalizeColor(style.nPlusOneColor, FALLBACK_COLORS.nPlusOneColor),
|
color: normalizeColor(style.nPlusOneColor, FALLBACK_COLORS.nPlusOneColor),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
shortcut: 'Character names',
|
||||||
|
action: normalizeColor(style.nameMatchColor, FALLBACK_COLORS.nameMatchColor),
|
||||||
|
color: normalizeColor(style.nameMatchColor, FALLBACK_COLORS.nameMatchColor),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
shortcut: 'JLPT N1',
|
shortcut: 'JLPT N1',
|
||||||
action: normalizeColor(style.jlptColors?.N1, FALLBACK_COLORS.jlptN1Color),
|
action: normalizeColor(style.jlptColors?.N1, FALLBACK_COLORS.jlptN1Color),
|
||||||
|
|||||||
@@ -58,6 +58,8 @@ export type RendererState = {
|
|||||||
|
|
||||||
knownWordColor: string;
|
knownWordColor: string;
|
||||||
nPlusOneColor: string;
|
nPlusOneColor: string;
|
||||||
|
nameMatchEnabled: boolean;
|
||||||
|
nameMatchColor: string;
|
||||||
jlptN1Color: string;
|
jlptN1Color: string;
|
||||||
jlptN2Color: string;
|
jlptN2Color: string;
|
||||||
jlptN3Color: string;
|
jlptN3Color: string;
|
||||||
@@ -125,6 +127,8 @@ export function createRendererState(): RendererState {
|
|||||||
|
|
||||||
knownWordColor: '#a6da95',
|
knownWordColor: '#a6da95',
|
||||||
nPlusOneColor: '#c6a0f6',
|
nPlusOneColor: '#c6a0f6',
|
||||||
|
nameMatchEnabled: true,
|
||||||
|
nameMatchColor: '#f5bde6',
|
||||||
jlptN1Color: '#ed8796',
|
jlptN1Color: '#ed8796',
|
||||||
jlptN2Color: '#f5a97f',
|
jlptN2Color: '#f5a97f',
|
||||||
jlptN3Color: '#f9e2af',
|
jlptN3Color: '#f9e2af',
|
||||||
|
|||||||
@@ -285,6 +285,7 @@ body {
|
|||||||
color: #cad3f5;
|
color: #cad3f5;
|
||||||
--subtitle-known-word-color: #a6da95;
|
--subtitle-known-word-color: #a6da95;
|
||||||
--subtitle-n-plus-one-color: #c6a0f6;
|
--subtitle-n-plus-one-color: #c6a0f6;
|
||||||
|
--subtitle-name-match-color: #f5bde6;
|
||||||
--subtitle-jlpt-n1-color: #ed8796;
|
--subtitle-jlpt-n1-color: #ed8796;
|
||||||
--subtitle-jlpt-n2-color: #f5a97f;
|
--subtitle-jlpt-n2-color: #f5a97f;
|
||||||
--subtitle-jlpt-n3-color: #f9e2af;
|
--subtitle-jlpt-n3-color: #f9e2af;
|
||||||
@@ -416,6 +417,11 @@ body.settings-modal-open #subtitleContainer {
|
|||||||
text-shadow: 0 0 6px rgba(198, 160, 246, 0.35);
|
text-shadow: 0 0 6px rgba(198, 160, 246, 0.35);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#subtitleRoot .word.word-name-match {
|
||||||
|
color: var(--subtitle-name-match-color, #f5bde6);
|
||||||
|
text-shadow: 0 0 6px rgba(245, 189, 230, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
#subtitleRoot .word.word-jlpt-n1 {
|
#subtitleRoot .word.word-jlpt-n1 {
|
||||||
text-decoration-line: underline;
|
text-decoration-line: underline;
|
||||||
text-decoration-thickness: 2px;
|
text-decoration-thickness: 2px;
|
||||||
@@ -510,7 +516,7 @@ body.settings-modal-open #subtitleContainer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#subtitleRoot
|
#subtitleRoot
|
||||||
.word:not(.word-known):not(.word-n-plus-one):not(.word-frequency-single):not(
|
.word:not(.word-known):not(.word-n-plus-one):not(.word-name-match):not(.word-frequency-single):not(
|
||||||
.word-frequency-band-1
|
.word-frequency-band-1
|
||||||
):not(.word-frequency-band-2):not(.word-frequency-band-3):not(.word-frequency-band-4):not(
|
):not(.word-frequency-band-2):not(.word-frequency-band-3):not(.word-frequency-band-4):not(
|
||||||
.word-frequency-band-5
|
.word-frequency-band-5
|
||||||
@@ -523,6 +529,7 @@ body.settings-modal-open #subtitleContainer {
|
|||||||
|
|
||||||
#subtitleRoot .word.word-known:hover,
|
#subtitleRoot .word.word-known:hover,
|
||||||
#subtitleRoot .word.word-n-plus-one:hover,
|
#subtitleRoot .word.word-n-plus-one:hover,
|
||||||
|
#subtitleRoot .word.word-name-match:hover,
|
||||||
#subtitleRoot .word.word-frequency-single:hover,
|
#subtitleRoot .word.word-frequency-single:hover,
|
||||||
#subtitleRoot .word.word-frequency-band-1:hover,
|
#subtitleRoot .word.word-frequency-band-1:hover,
|
||||||
#subtitleRoot .word.word-frequency-band-2:hover,
|
#subtitleRoot .word.word-frequency-band-2:hover,
|
||||||
@@ -536,6 +543,7 @@ body.settings-modal-open #subtitleContainer {
|
|||||||
|
|
||||||
#subtitleRoot .word.word-known .c:hover,
|
#subtitleRoot .word.word-known .c:hover,
|
||||||
#subtitleRoot .word.word-n-plus-one .c:hover,
|
#subtitleRoot .word.word-n-plus-one .c:hover,
|
||||||
|
#subtitleRoot .word.word-name-match .c:hover,
|
||||||
#subtitleRoot .word.word-frequency-single .c:hover,
|
#subtitleRoot .word.word-frequency-single .c:hover,
|
||||||
#subtitleRoot .word.word-frequency-band-1 .c:hover,
|
#subtitleRoot .word.word-frequency-band-1 .c:hover,
|
||||||
#subtitleRoot .word.word-frequency-band-2 .c:hover,
|
#subtitleRoot .word.word-frequency-band-2 .c:hover,
|
||||||
@@ -550,7 +558,7 @@ body.settings-modal-open #subtitleContainer {
|
|||||||
#subtitleRoot
|
#subtitleRoot
|
||||||
.word:is(.word-jlpt-n1, .word-jlpt-n2, .word-jlpt-n3, .word-jlpt-n4, .word-jlpt-n5):not(
|
.word:is(.word-jlpt-n1, .word-jlpt-n2, .word-jlpt-n3, .word-jlpt-n4, .word-jlpt-n5):not(
|
||||||
.word-known
|
.word-known
|
||||||
):not(.word-n-plus-one):not(.word-frequency-single):not(.word-frequency-band-1):not(
|
):not(.word-n-plus-one):not(.word-name-match):not(.word-frequency-single):not(.word-frequency-band-1):not(
|
||||||
.word-frequency-band-2
|
.word-frequency-band-2
|
||||||
):not(.word-frequency-band-3):not(.word-frequency-band-4):not(.word-frequency-band-5):hover {
|
):not(.word-frequency-band-3):not(.word-frequency-band-4):not(.word-frequency-band-5):hover {
|
||||||
color: var(--subtitle-hover-token-color, #f4dbd6) !important;
|
color: var(--subtitle-hover-token-color, #f4dbd6) !important;
|
||||||
@@ -583,6 +591,12 @@ body.settings-modal-open #subtitleContainer {
|
|||||||
-webkit-text-fill-color: var(--subtitle-n-plus-one-color, #c6a0f6) !important;
|
-webkit-text-fill-color: var(--subtitle-n-plus-one-color, #c6a0f6) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#subtitleRoot .word.word-name-match::selection,
|
||||||
|
#subtitleRoot .word.word-name-match .c::selection {
|
||||||
|
color: var(--subtitle-name-match-color, #f5bde6) !important;
|
||||||
|
-webkit-text-fill-color: var(--subtitle-name-match-color, #f5bde6) !important;
|
||||||
|
}
|
||||||
|
|
||||||
#subtitleRoot .word.word-frequency-single::selection,
|
#subtitleRoot .word.word-frequency-single::selection,
|
||||||
#subtitleRoot .word.word-frequency-single .c::selection {
|
#subtitleRoot .word.word-frequency-single .c::selection {
|
||||||
color: var(--subtitle-frequency-single-color, #f5a97f) !important;
|
color: var(--subtitle-frequency-single-color, #f5a97f) !important;
|
||||||
@@ -622,13 +636,13 @@ body.settings-modal-open #subtitleContainer {
|
|||||||
#subtitleRoot
|
#subtitleRoot
|
||||||
.word:is(.word-jlpt-n1, .word-jlpt-n2, .word-jlpt-n3, .word-jlpt-n4, .word-jlpt-n5):not(
|
.word:is(.word-jlpt-n1, .word-jlpt-n2, .word-jlpt-n3, .word-jlpt-n4, .word-jlpt-n5):not(
|
||||||
.word-known
|
.word-known
|
||||||
):not(.word-n-plus-one):not(.word-frequency-single):not(.word-frequency-band-1):not(
|
):not(.word-n-plus-one):not(.word-name-match):not(.word-frequency-single):not(.word-frequency-band-1):not(
|
||||||
.word-frequency-band-2
|
.word-frequency-band-2
|
||||||
):not(.word-frequency-band-3):not(.word-frequency-band-4):not(.word-frequency-band-5)::selection,
|
):not(.word-frequency-band-3):not(.word-frequency-band-4):not(.word-frequency-band-5)::selection,
|
||||||
#subtitleRoot
|
#subtitleRoot
|
||||||
.word:is(.word-jlpt-n1, .word-jlpt-n2, .word-jlpt-n3, .word-jlpt-n4, .word-jlpt-n5):not(
|
.word:is(.word-jlpt-n1, .word-jlpt-n2, .word-jlpt-n3, .word-jlpt-n4, .word-jlpt-n5):not(
|
||||||
.word-known
|
.word-known
|
||||||
):not(.word-n-plus-one):not(.word-frequency-single):not(.word-frequency-band-1):not(
|
):not(.word-n-plus-one):not(.word-name-match):not(.word-frequency-single):not(.word-frequency-band-1):not(
|
||||||
.word-frequency-band-2
|
.word-frequency-band-2
|
||||||
):not(.word-frequency-band-3):not(.word-frequency-band-4):not(.word-frequency-band-5)
|
):not(.word-frequency-band-3):not(.word-frequency-band-4):not(.word-frequency-band-5)
|
||||||
.c::selection {
|
.c::selection {
|
||||||
|
|||||||
@@ -181,6 +181,45 @@ test('computeWordClass preserves known and n+1 classes while adding JLPT classes
|
|||||||
assert.equal(computeWordClass(nPlusOneJlpt), 'word word-n-plus-one word-jlpt-n2');
|
assert.equal(computeWordClass(nPlusOneJlpt), 'word word-n-plus-one word-jlpt-n2');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('computeWordClass applies name-match class ahead of known and frequency classes', () => {
|
||||||
|
const token = createToken({
|
||||||
|
isKnown: true,
|
||||||
|
frequencyRank: 10,
|
||||||
|
surface: 'アクア',
|
||||||
|
}) as MergedToken & { isNameMatch?: boolean };
|
||||||
|
token.isNameMatch = true;
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
computeWordClass(token, {
|
||||||
|
enabled: true,
|
||||||
|
topX: 100,
|
||||||
|
mode: 'single',
|
||||||
|
singleColor: '#000000',
|
||||||
|
bandedColors: ['#000000', '#000000', '#000000', '#000000', '#000000'] as const,
|
||||||
|
}),
|
||||||
|
'word word-name-match',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('computeWordClass skips name-match class when disabled', () => {
|
||||||
|
const token = createToken({
|
||||||
|
surface: 'アクア',
|
||||||
|
}) as MergedToken & { isNameMatch?: boolean };
|
||||||
|
token.isNameMatch = true;
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
computeWordClass(token, {
|
||||||
|
nameMatchEnabled: false,
|
||||||
|
enabled: true,
|
||||||
|
topX: 100,
|
||||||
|
mode: 'single',
|
||||||
|
singleColor: '#000000',
|
||||||
|
bandedColors: ['#000000', '#000000', '#000000', '#000000', '#000000'] as const,
|
||||||
|
}),
|
||||||
|
'word',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test('computeWordClass keeps known and N+1 color classes exclusive over frequency classes', () => {
|
test('computeWordClass keeps known and N+1 color classes exclusive over frequency classes', () => {
|
||||||
const known = createToken({
|
const known = createToken({
|
||||||
isKnown: true,
|
isKnown: true,
|
||||||
@@ -229,6 +268,39 @@ test('computeWordClass keeps known and N+1 color classes exclusive over frequenc
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('applySubtitleStyle sets subtitle name-match color variable', () => {
|
||||||
|
const restoreDocument = installFakeDocument();
|
||||||
|
try {
|
||||||
|
const subtitleRoot = new FakeElement('div');
|
||||||
|
const subtitleContainer = new FakeElement('div');
|
||||||
|
const secondarySubRoot = new FakeElement('div');
|
||||||
|
const secondarySubContainer = new FakeElement('div');
|
||||||
|
const ctx = {
|
||||||
|
state: createRendererState(),
|
||||||
|
dom: {
|
||||||
|
subtitleRoot,
|
||||||
|
subtitleContainer,
|
||||||
|
secondarySubRoot,
|
||||||
|
secondarySubContainer,
|
||||||
|
},
|
||||||
|
} as never;
|
||||||
|
|
||||||
|
const renderer = createSubtitleRenderer(ctx);
|
||||||
|
renderer.applySubtitleStyle({
|
||||||
|
nameMatchColor: '#f5bde6',
|
||||||
|
} as never);
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
(subtitleRoot.style as unknown as { values?: Map<string, string> }).values?.get(
|
||||||
|
'--subtitle-name-match-color',
|
||||||
|
),
|
||||||
|
'#f5bde6',
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
restoreDocument();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test('computeWordClass adds frequency class for single mode when rank is within topX', () => {
|
test('computeWordClass adds frequency class for single mode when rank is within topX', () => {
|
||||||
const token = createToken({
|
const token = createToken({
|
||||||
surface: '猫',
|
surface: '猫',
|
||||||
@@ -598,7 +670,7 @@ test('JLPT CSS rules use underline-only styling in renderer stylesheet', () => {
|
|||||||
|
|
||||||
assert.match(
|
assert.match(
|
||||||
cssText,
|
cssText,
|
||||||
/#subtitleRoot\s+\.word:not\(\.word-known\):not\(\.word-n-plus-one\):not\(\.word-frequency-single\):not\(\s*\.word-frequency-band-1\s*\):not\(\.word-frequency-band-2\):not\(\.word-frequency-band-3\):not\(\.word-frequency-band-4\):not\(\s*\.word-frequency-band-5\s*\):hover\s*\{[\s\S]*?background:\s*var\(--subtitle-hover-token-background-color,\s*rgba\(54,\s*58,\s*79,\s*0\.84\)\);[\s\S]*?color:\s*var\(--subtitle-hover-token-color,\s*#f4dbd6\)\s*!important;[\s\S]*?-webkit-text-fill-color:\s*var\(--subtitle-hover-token-color,\s*#f4dbd6\)\s*!important;/,
|
/#subtitleRoot\s+\.word:not\(\.word-known\):not\(\.word-n-plus-one\):not\(\.word-name-match\):not\(\.word-frequency-single\):not\(\s*\.word-frequency-band-1\s*\):not\(\.word-frequency-band-2\):not\(\.word-frequency-band-3\):not\(\.word-frequency-band-4\):not\(\s*\.word-frequency-band-5\s*\):hover\s*\{[\s\S]*?background:\s*var\(--subtitle-hover-token-background-color,\s*rgba\(54,\s*58,\s*79,\s*0\.84\)\);[\s\S]*?color:\s*var\(--subtitle-hover-token-color,\s*#f4dbd6\)\s*!important;[\s\S]*?-webkit-text-fill-color:\s*var\(--subtitle-hover-token-color,\s*#f4dbd6\)\s*!important;/,
|
||||||
);
|
);
|
||||||
|
|
||||||
const coloredWordHoverBlock = extractClassBlock(cssText, '#subtitleRoot .word.word-known:hover');
|
const coloredWordHoverBlock = extractClassBlock(cssText, '#subtitleRoot .word.word-known:hover');
|
||||||
@@ -636,11 +708,11 @@ test('JLPT CSS rules use underline-only styling in renderer stylesheet', () => {
|
|||||||
|
|
||||||
assert.match(
|
assert.match(
|
||||||
cssText,
|
cssText,
|
||||||
/\.word:is\(\.word-jlpt-n1,\s*\.word-jlpt-n2,\s*\.word-jlpt-n3,\s*\.word-jlpt-n4,\s*\.word-jlpt-n5\):not\(\s*\.word-known\s*\):not\(\.word-n-plus-one\):not\(\.word-frequency-single\):not\(\.word-frequency-band-1\):not\(\s*\.word-frequency-band-2\s*\):not\(\.word-frequency-band-3\):not\(\.word-frequency-band-4\):not\(\.word-frequency-band-5\):hover\s*\{[\s\S]*?color:\s*var\(--subtitle-hover-token-color,\s*#f4dbd6\)\s*!important;[\s\S]*?-webkit-text-fill-color:\s*var\(--subtitle-hover-token-color,\s*#f4dbd6\)\s*!important;/,
|
/\.word:is\(\.word-jlpt-n1,\s*\.word-jlpt-n2,\s*\.word-jlpt-n3,\s*\.word-jlpt-n4,\s*\.word-jlpt-n5\):not\(\s*\.word-known\s*\):not\(\.word-n-plus-one\):not\(\.word-name-match\):not\(\.word-frequency-single\):not\(\.word-frequency-band-1\):not\(\s*\.word-frequency-band-2\s*\):not\(\.word-frequency-band-3\):not\(\.word-frequency-band-4\):not\(\.word-frequency-band-5\):hover\s*\{[\s\S]*?color:\s*var\(--subtitle-hover-token-color,\s*#f4dbd6\)\s*!important;[\s\S]*?-webkit-text-fill-color:\s*var\(--subtitle-hover-token-color,\s*#f4dbd6\)\s*!important;/,
|
||||||
);
|
);
|
||||||
assert.match(
|
assert.match(
|
||||||
cssText,
|
cssText,
|
||||||
/\.word:is\(\.word-jlpt-n1,\s*\.word-jlpt-n2,\s*\.word-jlpt-n3,\s*\.word-jlpt-n4,\s*\.word-jlpt-n5\):not\(\s*\.word-known\s*\):not\(\.word-n-plus-one\):not\(\.word-frequency-single\):not\(\.word-frequency-band-1\):not\(\s*\.word-frequency-band-2\s*\):not\(\.word-frequency-band-3\):not\(\.word-frequency-band-4\):not\(\.word-frequency-band-5\)::selection[\s\S]*?color:\s*var\(--subtitle-hover-token-color,\s*#f4dbd6\)\s*!important;[\s\S]*?-webkit-text-fill-color:\s*var\(--subtitle-hover-token-color,\s*#f4dbd6\)\s*!important;/,
|
/\.word:is\(\.word-jlpt-n1,\s*\.word-jlpt-n2,\s*\.word-jlpt-n3,\s*\.word-jlpt-n4,\s*\.word-jlpt-n5\):not\(\s*\.word-known\s*\):not\(\.word-n-plus-one\):not\(\.word-name-match\):not\(\.word-frequency-single\):not\(\.word-frequency-band-1\):not\(\s*\.word-frequency-band-2\s*\):not\(\.word-frequency-band-3\):not\(\.word-frequency-band-4\):not\(\.word-frequency-band-5\)::selection[\s\S]*?color:\s*var\(--subtitle-hover-token-color,\s*#f4dbd6\)\s*!important;[\s\S]*?-webkit-text-fill-color:\s*var\(--subtitle-hover-token-color,\s*#f4dbd6\)\s*!important;/,
|
||||||
);
|
);
|
||||||
|
|
||||||
const selectionBlock = extractClassBlock(cssText, '#subtitleRoot::selection');
|
const selectionBlock = extractClassBlock(cssText, '#subtitleRoot::selection');
|
||||||
|
|||||||
@@ -9,6 +9,10 @@ type FrequencyRenderSettings = {
|
|||||||
bandedColors: [string, string, string, string, string];
|
bandedColors: [string, string, string, string, string];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type TokenRenderSettings = FrequencyRenderSettings & {
|
||||||
|
nameMatchEnabled: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export type SubtitleTokenHoverRange = {
|
export type SubtitleTokenHoverRange = {
|
||||||
start: number;
|
start: number;
|
||||||
end: number;
|
end: number;
|
||||||
@@ -77,6 +81,7 @@ const DEFAULT_FREQUENCY_RENDER_SETTINGS: FrequencyRenderSettings = {
|
|||||||
singleColor: '#f5a97f',
|
singleColor: '#f5a97f',
|
||||||
bandedColors: ['#ed8796', '#f5a97f', '#f9e2af', '#8bd5ca', '#8aadf4'],
|
bandedColors: ['#ed8796', '#f5a97f', '#f9e2af', '#8bd5ca', '#8aadf4'],
|
||||||
};
|
};
|
||||||
|
const DEFAULT_NAME_MATCH_ENABLED = true;
|
||||||
|
|
||||||
function sanitizeFrequencyTopX(value: unknown, fallback: number): number {
|
function sanitizeFrequencyTopX(value: unknown, fallback: number): number {
|
||||||
if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) {
|
if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) {
|
||||||
@@ -218,25 +223,23 @@ export function getJlptLevelLabelForToken(token: MergedToken): string | null {
|
|||||||
function renderWithTokens(
|
function renderWithTokens(
|
||||||
root: HTMLElement,
|
root: HTMLElement,
|
||||||
tokens: MergedToken[],
|
tokens: MergedToken[],
|
||||||
frequencyRenderSettings?: Partial<FrequencyRenderSettings>,
|
tokenRenderSettings?: Partial<TokenRenderSettings>,
|
||||||
sourceText?: string,
|
sourceText?: string,
|
||||||
preserveLineBreaks = false,
|
preserveLineBreaks = false,
|
||||||
): void {
|
): void {
|
||||||
const resolvedFrequencyRenderSettings = {
|
const resolvedTokenRenderSettings = {
|
||||||
...DEFAULT_FREQUENCY_RENDER_SETTINGS,
|
...DEFAULT_FREQUENCY_RENDER_SETTINGS,
|
||||||
...frequencyRenderSettings,
|
...tokenRenderSettings,
|
||||||
bandedColors: sanitizeFrequencyBandedColors(
|
bandedColors: sanitizeFrequencyBandedColors(
|
||||||
frequencyRenderSettings?.bandedColors,
|
tokenRenderSettings?.bandedColors,
|
||||||
DEFAULT_FREQUENCY_RENDER_SETTINGS.bandedColors,
|
DEFAULT_FREQUENCY_RENDER_SETTINGS.bandedColors,
|
||||||
),
|
),
|
||||||
topX: sanitizeFrequencyTopX(
|
topX: sanitizeFrequencyTopX(tokenRenderSettings?.topX, DEFAULT_FREQUENCY_RENDER_SETTINGS.topX),
|
||||||
frequencyRenderSettings?.topX,
|
|
||||||
DEFAULT_FREQUENCY_RENDER_SETTINGS.topX,
|
|
||||||
),
|
|
||||||
singleColor: sanitizeHexColor(
|
singleColor: sanitizeHexColor(
|
||||||
frequencyRenderSettings?.singleColor,
|
tokenRenderSettings?.singleColor,
|
||||||
DEFAULT_FREQUENCY_RENDER_SETTINGS.singleColor,
|
DEFAULT_FREQUENCY_RENDER_SETTINGS.singleColor,
|
||||||
),
|
),
|
||||||
|
nameMatchEnabled: tokenRenderSettings?.nameMatchEnabled ?? DEFAULT_NAME_MATCH_ENABLED,
|
||||||
};
|
};
|
||||||
|
|
||||||
const fragment = document.createDocumentFragment();
|
const fragment = document.createDocumentFragment();
|
||||||
@@ -257,14 +260,14 @@ function renderWithTokens(
|
|||||||
|
|
||||||
const token = segment.token;
|
const token = segment.token;
|
||||||
const span = document.createElement('span');
|
const span = document.createElement('span');
|
||||||
span.className = computeWordClass(token, resolvedFrequencyRenderSettings);
|
span.className = computeWordClass(token, resolvedTokenRenderSettings);
|
||||||
span.textContent = token.surface;
|
span.textContent = token.surface;
|
||||||
span.dataset.tokenIndex = String(segment.tokenIndex);
|
span.dataset.tokenIndex = String(segment.tokenIndex);
|
||||||
if (token.reading) span.dataset.reading = token.reading;
|
if (token.reading) span.dataset.reading = token.reading;
|
||||||
if (token.headword) span.dataset.headword = token.headword;
|
if (token.headword) span.dataset.headword = token.headword;
|
||||||
const frequencyRankLabel = getFrequencyRankLabelForToken(
|
const frequencyRankLabel = getFrequencyRankLabelForToken(
|
||||||
token,
|
token,
|
||||||
resolvedFrequencyRenderSettings,
|
resolvedTokenRenderSettings,
|
||||||
);
|
);
|
||||||
if (frequencyRankLabel) {
|
if (frequencyRankLabel) {
|
||||||
span.dataset.frequencyRank = frequencyRankLabel;
|
span.dataset.frequencyRank = frequencyRankLabel;
|
||||||
@@ -296,14 +299,14 @@ function renderWithTokens(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const span = document.createElement('span');
|
const span = document.createElement('span');
|
||||||
span.className = computeWordClass(token, resolvedFrequencyRenderSettings);
|
span.className = computeWordClass(token, resolvedTokenRenderSettings);
|
||||||
span.textContent = surface;
|
span.textContent = surface;
|
||||||
span.dataset.tokenIndex = String(index);
|
span.dataset.tokenIndex = String(index);
|
||||||
if (token.reading) span.dataset.reading = token.reading;
|
if (token.reading) span.dataset.reading = token.reading;
|
||||||
if (token.headword) span.dataset.headword = token.headword;
|
if (token.headword) span.dataset.headword = token.headword;
|
||||||
const frequencyRankLabel = getFrequencyRankLabelForToken(
|
const frequencyRankLabel = getFrequencyRankLabelForToken(
|
||||||
token,
|
token,
|
||||||
resolvedFrequencyRenderSettings,
|
resolvedTokenRenderSettings,
|
||||||
);
|
);
|
||||||
if (frequencyRankLabel) {
|
if (frequencyRankLabel) {
|
||||||
span.dataset.frequencyRank = frequencyRankLabel;
|
span.dataset.frequencyRank = frequencyRankLabel;
|
||||||
@@ -401,26 +404,32 @@ export function buildSubtitleTokenHoverRanges(
|
|||||||
|
|
||||||
export function computeWordClass(
|
export function computeWordClass(
|
||||||
token: MergedToken,
|
token: MergedToken,
|
||||||
frequencySettings?: Partial<FrequencyRenderSettings>,
|
tokenRenderSettings?: Partial<TokenRenderSettings>,
|
||||||
): string {
|
): string {
|
||||||
const resolvedFrequencySettings = {
|
const resolvedTokenRenderSettings = {
|
||||||
...DEFAULT_FREQUENCY_RENDER_SETTINGS,
|
...DEFAULT_FREQUENCY_RENDER_SETTINGS,
|
||||||
...frequencySettings,
|
...tokenRenderSettings,
|
||||||
bandedColors: sanitizeFrequencyBandedColors(
|
bandedColors: sanitizeFrequencyBandedColors(
|
||||||
frequencySettings?.bandedColors,
|
tokenRenderSettings?.bandedColors,
|
||||||
DEFAULT_FREQUENCY_RENDER_SETTINGS.bandedColors,
|
DEFAULT_FREQUENCY_RENDER_SETTINGS.bandedColors,
|
||||||
),
|
),
|
||||||
topX: sanitizeFrequencyTopX(frequencySettings?.topX, DEFAULT_FREQUENCY_RENDER_SETTINGS.topX),
|
topX: sanitizeFrequencyTopX(
|
||||||
|
tokenRenderSettings?.topX,
|
||||||
|
DEFAULT_FREQUENCY_RENDER_SETTINGS.topX,
|
||||||
|
),
|
||||||
singleColor: sanitizeHexColor(
|
singleColor: sanitizeHexColor(
|
||||||
frequencySettings?.singleColor,
|
tokenRenderSettings?.singleColor,
|
||||||
DEFAULT_FREQUENCY_RENDER_SETTINGS.singleColor,
|
DEFAULT_FREQUENCY_RENDER_SETTINGS.singleColor,
|
||||||
),
|
),
|
||||||
|
nameMatchEnabled: tokenRenderSettings?.nameMatchEnabled ?? DEFAULT_NAME_MATCH_ENABLED,
|
||||||
};
|
};
|
||||||
|
|
||||||
const classes = ['word'];
|
const classes = ['word'];
|
||||||
|
|
||||||
if (token.isNPlusOneTarget) {
|
if (token.isNPlusOneTarget) {
|
||||||
classes.push('word-n-plus-one');
|
classes.push('word-n-plus-one');
|
||||||
|
} else if (resolvedTokenRenderSettings.nameMatchEnabled && token.isNameMatch) {
|
||||||
|
classes.push('word-name-match');
|
||||||
} else if (token.isKnown) {
|
} else if (token.isKnown) {
|
||||||
classes.push('word-known');
|
classes.push('word-known');
|
||||||
}
|
}
|
||||||
@@ -429,8 +438,12 @@ export function computeWordClass(
|
|||||||
classes.push(`word-jlpt-${token.jlptLevel.toLowerCase()}`);
|
classes.push(`word-jlpt-${token.jlptLevel.toLowerCase()}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!token.isKnown && !token.isNPlusOneTarget) {
|
if (
|
||||||
const frequencyClass = getFrequencyDictionaryClass(token, resolvedFrequencySettings);
|
!token.isKnown &&
|
||||||
|
!token.isNPlusOneTarget &&
|
||||||
|
!(resolvedTokenRenderSettings.nameMatchEnabled && token.isNameMatch)
|
||||||
|
) {
|
||||||
|
const frequencyClass = getFrequencyDictionaryClass(token, resolvedTokenRenderSettings);
|
||||||
if (frequencyClass) {
|
if (frequencyClass) {
|
||||||
classes.push(frequencyClass);
|
classes.push(frequencyClass);
|
||||||
}
|
}
|
||||||
@@ -494,7 +507,7 @@ export function createSubtitleRenderer(ctx: RendererContext) {
|
|||||||
renderWithTokens(
|
renderWithTokens(
|
||||||
ctx.dom.subtitleRoot,
|
ctx.dom.subtitleRoot,
|
||||||
tokens,
|
tokens,
|
||||||
getFrequencyRenderSettings(),
|
getTokenRenderSettings(),
|
||||||
text,
|
text,
|
||||||
ctx.state.preserveSubtitleLineBreaks,
|
ctx.state.preserveSubtitleLineBreaks,
|
||||||
);
|
);
|
||||||
@@ -503,8 +516,9 @@ export function createSubtitleRenderer(ctx: RendererContext) {
|
|||||||
renderCharacterLevel(ctx.dom.subtitleRoot, normalized);
|
renderCharacterLevel(ctx.dom.subtitleRoot, normalized);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFrequencyRenderSettings(): Partial<FrequencyRenderSettings> {
|
function getTokenRenderSettings(): Partial<TokenRenderSettings> {
|
||||||
return {
|
return {
|
||||||
|
nameMatchEnabled: ctx.state.nameMatchEnabled,
|
||||||
enabled: ctx.state.frequencyDictionaryEnabled,
|
enabled: ctx.state.frequencyDictionaryEnabled,
|
||||||
topX: ctx.state.frequencyDictionaryTopX,
|
topX: ctx.state.frequencyDictionaryTopX,
|
||||||
mode: ctx.state.frequencyDictionaryMode,
|
mode: ctx.state.frequencyDictionaryMode,
|
||||||
@@ -577,6 +591,8 @@ export function createSubtitleRenderer(ctx: RendererContext) {
|
|||||||
if (style.fontStyle) ctx.dom.subtitleRoot.style.fontStyle = style.fontStyle;
|
if (style.fontStyle) ctx.dom.subtitleRoot.style.fontStyle = style.fontStyle;
|
||||||
const knownWordColor = style.knownWordColor ?? ctx.state.knownWordColor ?? '#a6da95';
|
const knownWordColor = style.knownWordColor ?? ctx.state.knownWordColor ?? '#a6da95';
|
||||||
const nPlusOneColor = style.nPlusOneColor ?? ctx.state.nPlusOneColor ?? '#c6a0f6';
|
const nPlusOneColor = style.nPlusOneColor ?? ctx.state.nPlusOneColor ?? '#c6a0f6';
|
||||||
|
const nameMatchEnabled = style.nameMatchEnabled ?? ctx.state.nameMatchEnabled ?? true;
|
||||||
|
const nameMatchColor = style.nameMatchColor ?? ctx.state.nameMatchColor ?? '#f5bde6';
|
||||||
const hoverTokenColor = sanitizeSubtitleHoverTokenColor(style.hoverTokenColor);
|
const hoverTokenColor = sanitizeSubtitleHoverTokenColor(style.hoverTokenColor);
|
||||||
const hoverTokenBackgroundColor = sanitizeSubtitleHoverTokenBackgroundColor(
|
const hoverTokenBackgroundColor = sanitizeSubtitleHoverTokenBackgroundColor(
|
||||||
style.hoverTokenBackgroundColor,
|
style.hoverTokenBackgroundColor,
|
||||||
@@ -600,8 +616,11 @@ export function createSubtitleRenderer(ctx: RendererContext) {
|
|||||||
|
|
||||||
ctx.state.knownWordColor = knownWordColor;
|
ctx.state.knownWordColor = knownWordColor;
|
||||||
ctx.state.nPlusOneColor = nPlusOneColor;
|
ctx.state.nPlusOneColor = nPlusOneColor;
|
||||||
|
ctx.state.nameMatchEnabled = nameMatchEnabled;
|
||||||
|
ctx.state.nameMatchColor = nameMatchColor;
|
||||||
ctx.dom.subtitleRoot.style.setProperty('--subtitle-known-word-color', knownWordColor);
|
ctx.dom.subtitleRoot.style.setProperty('--subtitle-known-word-color', knownWordColor);
|
||||||
ctx.dom.subtitleRoot.style.setProperty('--subtitle-n-plus-one-color', nPlusOneColor);
|
ctx.dom.subtitleRoot.style.setProperty('--subtitle-n-plus-one-color', nPlusOneColor);
|
||||||
|
ctx.dom.subtitleRoot.style.setProperty('--subtitle-name-match-color', nameMatchColor);
|
||||||
ctx.dom.subtitleRoot.style.setProperty('--subtitle-hover-token-color', hoverTokenColor);
|
ctx.dom.subtitleRoot.style.setProperty('--subtitle-hover-token-color', hoverTokenColor);
|
||||||
ctx.dom.subtitleRoot.style.setProperty(
|
ctx.dom.subtitleRoot.style.setProperty(
|
||||||
'--subtitle-hover-token-background-color',
|
'--subtitle-hover-token-background-color',
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ export interface MergedToken {
|
|||||||
isMerged: boolean;
|
isMerged: boolean;
|
||||||
isKnown: boolean;
|
isKnown: boolean;
|
||||||
isNPlusOneTarget: boolean;
|
isNPlusOneTarget: boolean;
|
||||||
|
isNameMatch?: boolean;
|
||||||
jlptLevel?: JlptLevel;
|
jlptLevel?: JlptLevel;
|
||||||
frequencyRank?: number;
|
frequencyRank?: number;
|
||||||
}
|
}
|
||||||
@@ -293,6 +294,8 @@ export interface SubtitleStyleConfig {
|
|||||||
autoPauseVideoOnYomitanPopup?: boolean;
|
autoPauseVideoOnYomitanPopup?: boolean;
|
||||||
hoverTokenColor?: string;
|
hoverTokenColor?: string;
|
||||||
hoverTokenBackgroundColor?: string;
|
hoverTokenBackgroundColor?: string;
|
||||||
|
nameMatchEnabled?: boolean;
|
||||||
|
nameMatchColor?: string;
|
||||||
fontFamily?: string;
|
fontFamily?: string;
|
||||||
fontSize?: number;
|
fontSize?: number;
|
||||||
fontColor?: string;
|
fontColor?: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user