fix: align texthooker and stats formatting with CI expectations

This commit is contained in:
2026-03-18 19:01:29 -07:00
parent ec56970646
commit f4cce31d4a
11 changed files with 225 additions and 46 deletions

View File

@@ -1,23 +1,72 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { injectTexthookerBootstrapHtml } from './texthooker';
import { injectTexthookerBootstrapHtml, type TexthookerBootstrapSettings } from './texthooker';
test('injectTexthookerBootstrapHtml injects websocket bootstrap before head close', () => {
const html = '<html><head><title>Texthooker</title></head><body></body></html>';
const actual = injectTexthookerBootstrapHtml(html, 'ws://127.0.0.1:6678');
const settings: TexthookerBootstrapSettings = {
enableKnownWordColoring: true,
enableNPlusOneColoring: true,
enableNameMatchColoring: true,
enableFrequencyColoring: true,
enableJlptColoring: true,
characterDictionaryEnabled: true,
knownWordColor: '#a6da95',
nPlusOneColor: '#c6a0f6',
nameMatchColor: '#f5bde6',
hoverTokenColor: '#f4dbd6',
hoverTokenBackgroundColor: 'rgba(54, 58, 79, 0.84)',
jlptColors: {
N1: '#ed8796',
N2: '#f5a97f',
N3: '#f9e2af',
N4: '#a6e3a1',
N5: '#8aadf4',
},
frequencyDictionary: {
singleColor: '#f5a97f',
bandedColors: ['#ed8796', '#f5a97f', '#f9e2af', '#8bd5ca', '#8aadf4'],
},
};
const actual = injectTexthookerBootstrapHtml(html, 'ws://127.0.0.1:6678', settings);
assert.match(
actual,
/window\.localStorage\.setItem\('bannou-texthooker-websocketUrl', "ws:\/\/127\.0\.0\.1:6678"\)/,
);
assert.match(
actual,
/window\.localStorage\.setItem\('bannou-texthooker-enableKnownWordColoring', "1"\)/,
);
assert.match(
actual,
/window\.localStorage\.setItem\('bannou-texthooker-enableNPlusOneColoring', "1"\)/,
);
assert.match(
actual,
/window\.localStorage\.setItem\('bannou-texthooker-enableNameMatchColoring', "1"\)/,
);
assert.match(
actual,
/window\.localStorage\.setItem\('bannou-texthooker-enableFrequencyColoring', "1"\)/,
);
assert.match(
actual,
/window\.localStorage\.setItem\('bannou-texthooker-enableJlptColoring', "1"\)/,
);
assert.match(
actual,
/window\.localStorage\.setItem\('bannou-texthooker-characterDictionaryEnabled', "1"\)/,
);
assert.match(actual, /--subminer-known-word-color:\s*#a6da95;/);
assert.match(actual, /--subminer-n-plus-one-color:\s*#c6a0f6;/);
assert.match(actual, /--subminer-name-match-color:\s*#f5bde6;/);
assert.match(actual, /--subminer-jlpt-n1-color:\s*#ed8796;/);
assert.match(actual, /--subminer-frequency-band-4-color:\s*#8bd5ca;/);
assert.match(actual, /--sm-token-hover-bg:\s*rgba\(54, 58, 79, 0\.84\);/);
assert.match(actual, /p \.word\.word-known\s*\{\s*color:\s*var\(--subminer-known-word-color\);/);
assert.ok(actual.indexOf('</script></head>') !== -1);
assert.ok(actual.includes('bannou-texthooker-websocketUrl'));
assert.ok(!actual.includes('bannou-texthooker-enableKnownWordColoring'));
assert.ok(!actual.includes('bannou-texthooker-enableNPlusOneColoring'));
assert.ok(!actual.includes('bannou-texthooker-enableNameMatchColoring'));
assert.ok(!actual.includes('bannou-texthooker-enableFrequencyColoring'));
assert.ok(!actual.includes('bannou-texthooker-enableJlptColoring'));
});
test('injectTexthookerBootstrapHtml leaves html unchanged without websocketUrl', () => {

View File

@@ -5,23 +5,92 @@ import { createLogger } from '../../logger';
const logger = createLogger('main:texthooker');
export function injectTexthookerBootstrapHtml(html: string, websocketUrl?: string): string {
if (!websocketUrl) {
export type TexthookerBootstrapSettings = {
enableKnownWordColoring: boolean;
enableNPlusOneColoring: boolean;
enableNameMatchColoring: boolean;
enableFrequencyColoring: boolean;
enableJlptColoring: boolean;
characterDictionaryEnabled: boolean;
knownWordColor: string;
nPlusOneColor: string;
nameMatchColor: string;
hoverTokenColor: string;
hoverTokenBackgroundColor: string;
jlptColors: {
N1: string;
N2: string;
N3: string;
N4: string;
N5: string;
};
frequencyDictionary: {
singleColor: string;
bandedColors: readonly [string, string, string, string, string];
};
};
function buildTexthookerBootstrapScript(
websocketUrl?: string,
settings?: TexthookerBootstrapSettings,
): string {
const statements: string[] = [];
if (websocketUrl) {
statements.push(
`window.localStorage.setItem('bannou-texthooker-websocketUrl', ${JSON.stringify(websocketUrl)});`,
);
}
if (settings) {
const booleanStorageValue = (enabled: boolean): '"1"' | '"0"' => (enabled ? '"1"' : '"0"');
statements.push(
`window.localStorage.setItem('bannou-texthooker-enableKnownWordColoring', ${booleanStorageValue(settings.enableKnownWordColoring)});`,
`window.localStorage.setItem('bannou-texthooker-enableNPlusOneColoring', ${booleanStorageValue(settings.enableNPlusOneColoring)});`,
`window.localStorage.setItem('bannou-texthooker-enableNameMatchColoring', ${booleanStorageValue(settings.enableNameMatchColoring)});`,
`window.localStorage.setItem('bannou-texthooker-enableFrequencyColoring', ${booleanStorageValue(settings.enableFrequencyColoring)});`,
`window.localStorage.setItem('bannou-texthooker-enableJlptColoring', ${booleanStorageValue(settings.enableJlptColoring)});`,
`window.localStorage.setItem('bannou-texthooker-characterDictionaryEnabled', ${booleanStorageValue(settings.characterDictionaryEnabled)});`,
);
}
return statements.length > 0 ? `<script>${statements.join('')}</script>` : '';
}
function buildTexthookerBootstrapStyle(settings?: TexthookerBootstrapSettings): string {
if (!settings) {
return '';
}
const [band1, band2, band3, band4, band5] = settings.frequencyDictionary.bandedColors;
return `<style id="subminer-texthooker-bootstrap-style">:root{--subminer-known-word-color:${settings.knownWordColor};--subminer-n-plus-one-color:${settings.nPlusOneColor};--subminer-name-match-color:${settings.nameMatchColor};--subminer-jlpt-n1-color:${settings.jlptColors.N1};--subminer-jlpt-n2-color:${settings.jlptColors.N2};--subminer-jlpt-n3-color:${settings.jlptColors.N3};--subminer-jlpt-n4-color:${settings.jlptColors.N4};--subminer-jlpt-n5-color:${settings.jlptColors.N5};--subminer-frequency-single-color:${settings.frequencyDictionary.singleColor};--subminer-frequency-band-1-color:${band1};--subminer-frequency-band-2-color:${band2};--subminer-frequency-band-3-color:${band3};--subminer-frequency-band-4-color:${band4};--subminer-frequency-band-5-color:${band5};--sm-token-hover-bg:${settings.hoverTokenBackgroundColor};--sm-token-hover-text:${settings.hoverTokenColor};}p .word.word-known{color:var(--subminer-known-word-color);}p .word.word-n-plus-one{color:var(--subminer-n-plus-one-color);}p .word.word-name-match{color:var(--subminer-name-match-color);}p .word.word-jlpt-n1{text-decoration-color:var(--subminer-jlpt-n1-color);}p .word.word-jlpt-n1[data-jlpt-level]::after{color:var(--subminer-jlpt-n1-color);}p .word.word-jlpt-n2{text-decoration-color:var(--subminer-jlpt-n2-color);}p .word.word-jlpt-n2[data-jlpt-level]::after{color:var(--subminer-jlpt-n2-color);}p .word.word-jlpt-n3{text-decoration-color:var(--subminer-jlpt-n3-color);}p .word.word-jlpt-n3[data-jlpt-level]::after{color:var(--subminer-jlpt-n3-color);}p .word.word-jlpt-n4{text-decoration-color:var(--subminer-jlpt-n4-color);}p .word.word-jlpt-n4[data-jlpt-level]::after{color:var(--subminer-jlpt-n4-color);}p .word.word-jlpt-n5{text-decoration-color:var(--subminer-jlpt-n5-color);}p .word.word-jlpt-n5[data-jlpt-level]::after{color:var(--subminer-jlpt-n5-color);}p .word.word-frequency-single{color:var(--subminer-frequency-single-color);}p .word.word-frequency-band-1{color:var(--subminer-frequency-band-1-color);}p .word.word-frequency-band-2{color:var(--subminer-frequency-band-2-color);}p .word.word-frequency-band-3{color:var(--subminer-frequency-band-3-color);}p .word.word-frequency-band-4{color:var(--subminer-frequency-band-4-color);}p .word.word-frequency-band-5{color:var(--subminer-frequency-band-5-color);}</style>`;
}
export function injectTexthookerBootstrapHtml(
html: string,
websocketUrl?: string,
settings?: TexthookerBootstrapSettings,
): string {
const bootstrapStyle = buildTexthookerBootstrapStyle(settings);
const bootstrapScript = buildTexthookerBootstrapScript(websocketUrl, settings);
if (!bootstrapStyle && !bootstrapScript) {
return html;
}
const bootstrapScript = `<script>window.localStorage.setItem('bannou-texthooker-websocketUrl', ${JSON.stringify(
websocketUrl,
)});</script>`;
if (html.includes('</head>')) {
return html.replace('</head>', `${bootstrapScript}</head>`);
return html.replace('</head>', `${bootstrapStyle}${bootstrapScript}</head>`);
}
return `${bootstrapScript}${html}`;
return `${bootstrapStyle}${bootstrapScript}${html}`;
}
export class Texthooker {
constructor(
private readonly getBootstrapSettings?: () => TexthookerBootstrapSettings | undefined,
) {}
private server: http.Server | null = null;
public isRunning(): boolean {
@@ -62,9 +131,16 @@ export class Texthooker {
res.end('Not found');
return;
}
const bootstrapSettings = this.getBootstrapSettings?.();
const responseData =
urlPath === '/' || urlPath === '/index.html'
? Buffer.from(injectTexthookerBootstrapHtml(data.toString('utf-8'), websocketUrl))
? Buffer.from(
injectTexthookerBootstrapHtml(
data.toString('utf-8'),
websocketUrl,
bootstrapSettings,
),
)
: data;
res.writeHead(200, { 'Content-Type': mimeTypes[ext] || 'text/plain' });
res.end(responseData);

View File

@@ -568,7 +568,40 @@ const anilistUpdateQueue = createAnilistUpdateQueue(
},
);
const isDev = process.argv.includes('--dev') || process.argv.includes('--debug');
const texthookerService = new Texthooker();
const texthookerService = new Texthooker(() => {
const config = getResolvedConfig();
const characterDictionaryEnabled =
config.anilist.characterDictionary.enabled && yomitanProfilePolicy.isCharacterDictionaryEnabled();
const knownAndNPlusOneEnabled = getRuntimeBooleanOption(
'subtitle.annotation.nPlusOne',
config.ankiConnect.knownWords.highlightEnabled,
);
return {
enableKnownWordColoring: knownAndNPlusOneEnabled,
enableNPlusOneColoring: knownAndNPlusOneEnabled,
enableNameMatchColoring: config.subtitleStyle.nameMatchEnabled && characterDictionaryEnabled,
enableFrequencyColoring: getRuntimeBooleanOption(
'subtitle.annotation.frequency',
config.subtitleStyle.frequencyDictionary.enabled,
),
enableJlptColoring: getRuntimeBooleanOption(
'subtitle.annotation.jlpt',
config.subtitleStyle.enableJlpt,
),
characterDictionaryEnabled,
knownWordColor: config.ankiConnect.knownWords.color,
nPlusOneColor: config.ankiConnect.nPlusOne.nPlusOne,
nameMatchColor: config.subtitleStyle.nameMatchColor,
hoverTokenColor: config.subtitleStyle.hoverTokenColor,
hoverTokenBackgroundColor: config.subtitleStyle.hoverTokenBackgroundColor,
jlptColors: config.subtitleStyle.jlptColors,
frequencyDictionary: {
singleColor: config.subtitleStyle.frequencyDictionary.singleColor,
bandedColors: config.subtitleStyle.frequencyDictionary.bandedColors,
},
};
});
const subtitleWsService = new SubtitleWebSocket();
const annotationSubtitleWsService = new SubtitleWebSocket();
const logger = createLogger('main');