mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-09-08 17:16:18 -07:00
fix(subtitles): keep overlapping lines that join an already active cue (#221)
This commit is contained in:
@@ -11,6 +11,7 @@ import {
|
||||
getFrequencyRankLabelForToken,
|
||||
getJlptLevelLabelForToken,
|
||||
normalizeSubtitle,
|
||||
normalizeSubtitleForDisplay,
|
||||
prepareSecondarySubtitleLines,
|
||||
sanitizeSubtitleHoverTokenColor,
|
||||
shouldRenderTokenizedSubtitle,
|
||||
@@ -1004,6 +1005,34 @@ test('normalizeSubtitle collapses explicit line breaks when collapseLineBreaks i
|
||||
);
|
||||
});
|
||||
|
||||
test('normalizeSubtitleForDisplay always breaks between simultaneous cues', () => {
|
||||
// The blank line marks two distinct cues on screen at once. Flattening it would run a
|
||||
// sign or a second speaker into the line beside it as one sentence.
|
||||
const twoCues =
|
||||
'\u6b21\u306f\u9b3c\u5b50\u6bcd\u795e\u524d\u3000\u9b3c\u5b50\u6bcd\u795e\u524d\n\n\u611b\u97f3\u3061\u3083\u3093\u3000\u3082\u3046\u5199\u771f\u4e0a\u3052\u3066\u308b';
|
||||
|
||||
assert.equal(
|
||||
normalizeSubtitleForDisplay(twoCues, false),
|
||||
'\u6b21\u306f\u9b3c\u5b50\u6bcd\u795e\u524d \u9b3c\u5b50\u6bcd\u795e\u524d\n\u611b\u97f3\u3061\u3083\u3093 \u3082\u3046\u5199\u771f\u4e0a\u3052\u3066\u308b',
|
||||
);
|
||||
assert.equal(normalizeSubtitleForDisplay(twoCues, true), twoCues.replace('\n\n', '\n'));
|
||||
});
|
||||
|
||||
test('normalizeSubtitleForDisplay preserves CRLF boundaries between simultaneous cues', () => {
|
||||
assert.equal(normalizeSubtitleForDisplay('a\r\n\r\nb', false), 'a\nb');
|
||||
});
|
||||
|
||||
test('normalizeSubtitleForDisplay still flattens a wrap inside one cue', () => {
|
||||
// A typesetter's \\N inside a single utterance is what preserveLineBreaks governs.
|
||||
assert.equal(
|
||||
normalizeSubtitleForDisplay(
|
||||
'\u5e38\u4eba\u304c\u4f7f\u3048\u3070\\N\u305d\u306e\u5727\u5012\u7684\u306a\u529b\u306b',
|
||||
false,
|
||||
),
|
||||
'\u5e38\u4eba\u304c\u4f7f\u3048\u3070 \u305d\u306e\u5727\u5012\u7684\u306a\u529b\u306b',
|
||||
);
|
||||
});
|
||||
|
||||
test('normalizeSubtitle leaves already-decoded text alone', () => {
|
||||
// Primary subtitle text is decoded from ASS once, upstream: by mpv for live lines and
|
||||
// by the cue parser for prefetched ones. A brace that survives that is literal text.
|
||||
|
||||
@@ -50,6 +50,21 @@ export function normalizeSubtitle(text: string, trim = true, collapseLineBreaks
|
||||
return normalizePlainSubtitleText(text, { trim, collapseLineBreaks });
|
||||
}
|
||||
|
||||
/**
|
||||
* Display form of a resolved subtitle. `preserveLineBreaks` governs wrapping inside one
|
||||
* utterance, which is what a typesetter's `\N` means. The blank line the resolver puts
|
||||
* between two simultaneous cues is a different thing and always breaks, so a sign or a
|
||||
* second speaker never runs into the line beside it.
|
||||
*/
|
||||
export function normalizeSubtitleForDisplay(text: string, preserveLineBreaks: boolean): string {
|
||||
return text
|
||||
.replace(/\r\n/g, '\n')
|
||||
.split(/\n{2,}/)
|
||||
.map((cueText) => normalizeSubtitle(cueText, true, !preserveLineBreaks))
|
||||
.filter((cueText) => cueText.length > 0)
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
const HEX_COLOR_PATTERN = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;
|
||||
const SAFE_CSS_COLOR_PATTERN =
|
||||
/^(?:#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|(?:rgba?|hsla?)\([^)]*\)|var\([^)]*\)|[a-zA-Z]+)$/;
|
||||
@@ -414,16 +429,13 @@ function renderWithTokens(
|
||||
const fragment = document.createDocumentFragment();
|
||||
|
||||
if (sourceText) {
|
||||
const normalizedSource = normalizeSubtitle(sourceText, true, !preserveLineBreaks);
|
||||
const normalizedSource = normalizeSubtitleForDisplay(sourceText, preserveLineBreaks);
|
||||
const segments = alignTokensToSourceText(tokens, normalizedSource);
|
||||
|
||||
for (const segment of segments) {
|
||||
if (segment.kind === 'text') {
|
||||
if (preserveLineBreaks) {
|
||||
renderPlainTextPreserveLineBreaks(fragment, segment.text);
|
||||
} else {
|
||||
fragment.appendChild(document.createTextNode(segment.text));
|
||||
}
|
||||
// Normalization already resolved which breaks survive; every one left is real.
|
||||
renderPlainTextPreserveLineBreaks(fragment, segment.text);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -748,7 +760,7 @@ export function createSubtitleRenderer(ctx: RendererContext) {
|
||||
return;
|
||||
}
|
||||
|
||||
const normalized = normalizeSubtitle(text, true, !ctx.state.preserveSubtitleLineBreaks);
|
||||
const normalized = normalizeSubtitleForDisplay(text, ctx.state.preserveSubtitleLineBreaks);
|
||||
const hasRenderableTokens =
|
||||
shouldRenderTokenizedSubtitle(tokens?.length ?? 0) && Boolean(tokens);
|
||||
if (
|
||||
|
||||
Reference in New Issue
Block a user