mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-02-27 18:22:41 -08:00
fix(renderer): stabilize preserveLineBreaks whitespace and newline rendering
This commit is contained in:
@@ -5,7 +5,7 @@ import path from 'node:path';
|
||||
|
||||
import type { MergedToken } from '../types';
|
||||
import { PartOfSpeech } from '../types.js';
|
||||
import { alignTokensToSourceText, computeWordClass } from './subtitle-render.js';
|
||||
import { alignTokensToSourceText, computeWordClass, normalizeSubtitle } from './subtitle-render.js';
|
||||
|
||||
function createToken(overrides: Partial<MergedToken>): MergedToken {
|
||||
return {
|
||||
@@ -216,6 +216,45 @@ test('alignTokensToSourceText preserves newline separators between adjacent toke
|
||||
);
|
||||
});
|
||||
|
||||
test('alignTokensToSourceText treats whitespace-only token surfaces as plain text separators', () => {
|
||||
const tokens = [
|
||||
createToken({ surface: '常人が使えば' }),
|
||||
createToken({ surface: ' ' }),
|
||||
createToken({ surface: 'その圧倒的な力に' }),
|
||||
createToken({ surface: '\n' }),
|
||||
createToken({ surface: '体が耐えきれず死に至るが…' }),
|
||||
];
|
||||
|
||||
const segments = alignTokensToSourceText(tokens, '常人が使えば その圧倒的な力に\n体が耐えきれず死に至るが…');
|
||||
assert.deepEqual(
|
||||
segments.map((segment) => (segment.kind === 'text' ? `text:${segment.text}` : 'token')),
|
||||
['token', 'text: ', 'token', 'text:\n', 'token'],
|
||||
);
|
||||
});
|
||||
|
||||
test('alignTokensToSourceText avoids duplicate tail when later token surface does not match source', () => {
|
||||
const tokens = [
|
||||
createToken({ surface: '君たちが潰した拠点に' }),
|
||||
createToken({ surface: '教団の主力は1人もいない' }),
|
||||
];
|
||||
|
||||
const segments = alignTokensToSourceText(
|
||||
tokens,
|
||||
'君たちが潰した拠点に\n教団の主力は1人もいない',
|
||||
);
|
||||
assert.deepEqual(
|
||||
segments.map((segment) => (segment.kind === 'text' ? `text:${segment.text}` : 'token')),
|
||||
['token', 'text:\n教団の主力は1人もいない'],
|
||||
);
|
||||
});
|
||||
|
||||
test('normalizeSubtitle collapses explicit line breaks when collapseLineBreaks is enabled', () => {
|
||||
assert.equal(
|
||||
normalizeSubtitle('常人が使えば\\Nその圧倒的な力に\\n体が耐えきれず死に至るが…', true, true),
|
||||
'常人が使えば その圧倒的な力に 体が耐えきれず死に至るが…',
|
||||
);
|
||||
});
|
||||
|
||||
test('JLPT CSS rules use underline-only styling in renderer stylesheet', () => {
|
||||
const distCssPath = path.join(process.cwd(), 'dist', 'renderer', 'style.css');
|
||||
const srcCssPath = path.join(process.cwd(), 'src', 'renderer', 'style.css');
|
||||
|
||||
@@ -9,7 +9,11 @@ type FrequencyRenderSettings = {
|
||||
bandedColors: [string, string, string, string, string];
|
||||
};
|
||||
|
||||
function normalizeSubtitle(text: string, trim = true, collapseLineBreaks = false): string {
|
||||
function isWhitespaceOnly(value: string): boolean {
|
||||
return value.trim().length === 0;
|
||||
}
|
||||
|
||||
export function normalizeSubtitle(text: string, trim = true, collapseLineBreaks = false): string {
|
||||
if (!text) return '';
|
||||
|
||||
let normalized = text.replace(/\\N/g, '\n').replace(/\\n/g, '\n');
|
||||
@@ -140,24 +144,13 @@ function renderWithTokens(
|
||||
}
|
||||
|
||||
for (const token of tokens) {
|
||||
const surface = token.surface;
|
||||
const surface = token.surface.replace(/\n/g, ' ');
|
||||
if (!surface) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (surface.includes('\n')) {
|
||||
const parts = surface.split('\n');
|
||||
for (let i = 0; i < parts.length; i += 1) {
|
||||
const part = parts[i];
|
||||
if (part) {
|
||||
const span = document.createElement('span');
|
||||
span.className = computeWordClass(token, resolvedFrequencyRenderSettings);
|
||||
span.textContent = part;
|
||||
if (token.reading) span.dataset.reading = token.reading;
|
||||
if (token.headword) span.dataset.headword = token.headword;
|
||||
fragment.appendChild(span);
|
||||
}
|
||||
if (i < parts.length - 1) {
|
||||
fragment.appendChild(document.createElement('br'));
|
||||
}
|
||||
}
|
||||
if (isWhitespaceOnly(surface)) {
|
||||
fragment.appendChild(document.createTextNode(surface));
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -187,17 +180,14 @@ export function alignTokensToSourceText(
|
||||
|
||||
for (const token of tokens) {
|
||||
const surface = token.surface;
|
||||
if (!surface) {
|
||||
if (!surface || isWhitespaceOnly(surface)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const foundIndex = sourceText.indexOf(surface, cursor);
|
||||
if (foundIndex < 0) {
|
||||
if (cursor < sourceText.length) {
|
||||
segments.push({ kind: 'text', text: sourceText.slice(cursor) });
|
||||
}
|
||||
segments.push({ kind: 'token', token });
|
||||
cursor = sourceText.length;
|
||||
// Token text can diverge from source normalization (e.g., half/full-width forms).
|
||||
// Skip unmatched token to avoid duplicating visible tail text in preserve-line-break mode.
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -318,7 +308,7 @@ export function createSubtitleRenderer(ctx: RendererContext) {
|
||||
return;
|
||||
}
|
||||
|
||||
const normalized = normalizeSubtitle(text);
|
||||
const normalized = normalizeSubtitle(text, true, !ctx.state.preserveSubtitleLineBreaks);
|
||||
if (tokens && tokens.length > 0) {
|
||||
renderWithTokens(
|
||||
ctx.dom.subtitleRoot,
|
||||
|
||||
Reference in New Issue
Block a user