diff --git a/plugin/subminer/hover.lua b/plugin/subminer/hover.lua index e13b103..6a24e41 100644 --- a/plugin/subminer/hover.lua +++ b/plugin/subminer/hover.lua @@ -249,36 +249,22 @@ function M.create(ctx) raw_close_idx = #raw_ass + 1 end - local open_tag = string.format("{\\1c&H%s&}", hover_color) - local close_tag = string.format("{\\1c&H%s&}", base_color) - local changes = { - { idx = raw_open_idx, tag = open_tag }, - { idx = raw_close_idx, tag = close_tag }, - } - table.sort(changes, function(a, b) - return a.idx < b.idx + local before = raw_ass:sub(1, raw_open_idx - 1) + local hovered = raw_ass:sub(raw_open_idx, raw_close_idx - 1) + local after = raw_ass:sub(raw_close_idx) + local hover_suffix = string.format("\\1c&H%s&", hover_color) + + -- Keep hover foreground stable even when inline ASS override tags (\1c/\c/\r) appear inside token. + hovered = hovered:gsub("{([^}]*)}", function(inner) + if inner:find("\\1c&H", 1, true) or inner:find("\\c&H", 1, true) or inner:find("\\r", 1, true) then + return "{" .. inner .. hover_suffix .. "}" + end + return "{" .. inner .. "}" end) - local output = {} - local cursor = 1 - for _, change in ipairs(changes) do - if change.idx > #raw_ass + 1 then - change.idx = #raw_ass + 1 - end - if change.idx < 1 then - change.idx = 1 - end - if change.idx > cursor then - output[#output + 1] = raw_ass:sub(cursor, change.idx - 1) - end - output[#output + 1] = change.tag - cursor = change.idx - end - if cursor <= #raw_ass then - output[#output + 1] = raw_ass:sub(cursor) - end - - return table.concat(output) + local open_tag = string.format("{\\1c&H%s&}", hover_color) + local close_tag = string.format("{\\1c&H%s&}", base_color) + return before .. open_tag .. hovered .. close_tag .. after end local function build_hover_subtitle_content(payload) diff --git a/src/renderer/style.css b/src/renderer/style.css index c932eb0..d6a288c 100644 --- a/src/renderer/style.css +++ b/src/renderer/style.css @@ -406,7 +406,6 @@ body.settings-modal-open #subtitleContainer { } #subtitleRoot .word.word-jlpt-n1 { - color: inherit; text-decoration-line: underline; text-decoration-thickness: 2px; text-underline-offset: 4px; @@ -419,7 +418,6 @@ body.settings-modal-open #subtitleContainer { } #subtitleRoot .word.word-jlpt-n2 { - color: inherit; text-decoration-line: underline; text-decoration-thickness: 2px; text-underline-offset: 4px; @@ -432,7 +430,6 @@ body.settings-modal-open #subtitleContainer { } #subtitleRoot .word.word-jlpt-n3 { - color: inherit; text-decoration-line: underline; text-decoration-thickness: 2px; text-underline-offset: 4px; @@ -445,7 +442,6 @@ body.settings-modal-open #subtitleContainer { } #subtitleRoot .word.word-jlpt-n4 { - color: inherit; text-decoration-line: underline; text-decoration-thickness: 2px; text-underline-offset: 4px; @@ -458,7 +454,6 @@ body.settings-modal-open #subtitleContainer { } #subtitleRoot .word.word-jlpt-n5 { - color: inherit; text-decoration-line: underline; text-decoration-thickness: 2px; text-underline-offset: 4px; @@ -540,6 +535,16 @@ body.settings-modal-open #subtitleContainer { -webkit-text-fill-color: currentColor !important; } +#subtitleRoot + .word:is(.word-jlpt-n1, .word-jlpt-n2, .word-jlpt-n3, .word-jlpt-n4, .word-jlpt-n5):not( + .word-known + ):not(.word-n-plus-one):not(.word-frequency-single):not(.word-frequency-band-1):not( + .word-frequency-band-2 + ):not(.word-frequency-band-3):not(.word-frequency-band-4):not(.word-frequency-band-5):hover { + color: var(--subtitle-hover-token-color, #f4dbd6) !important; + -webkit-text-fill-color: var(--subtitle-hover-token-color, #f4dbd6) !important; +} + #subtitleRoot::selection, #subtitleRoot .word::selection, #subtitleRoot .c::selection { @@ -602,6 +607,23 @@ body.settings-modal-open #subtitleContainer { -webkit-text-fill-color: var(--subtitle-frequency-band-5-color, #8aadf4) !important; } +#subtitleRoot + .word:is(.word-jlpt-n1, .word-jlpt-n2, .word-jlpt-n3, .word-jlpt-n4, .word-jlpt-n5):not( + .word-known + ):not(.word-n-plus-one):not(.word-frequency-single):not(.word-frequency-band-1):not( + .word-frequency-band-2 + ):not(.word-frequency-band-3):not(.word-frequency-band-4):not(.word-frequency-band-5)::selection, +#subtitleRoot + .word:is(.word-jlpt-n1, .word-jlpt-n2, .word-jlpt-n3, .word-jlpt-n4, .word-jlpt-n5):not( + .word-known + ):not(.word-n-plus-one):not(.word-frequency-single):not(.word-frequency-band-1):not( + .word-frequency-band-2 + ):not(.word-frequency-band-3):not(.word-frequency-band-4):not(.word-frequency-band-5) + .c::selection { + color: var(--subtitle-hover-token-color, #f4dbd6) !important; + -webkit-text-fill-color: var(--subtitle-hover-token-color, #f4dbd6) !important; +} + #subtitleRoot br { display: block; content: ''; diff --git a/src/renderer/subtitle-render.test.ts b/src/renderer/subtitle-render.test.ts index c5e2359..8783987 100644 --- a/src/renderer/subtitle-render.test.ts +++ b/src/renderer/subtitle-render.test.ts @@ -353,7 +353,7 @@ test('JLPT CSS rules use underline-only styling in renderer stylesheet', () => { assert.match(block, /text-decoration-line:\s*underline;/); assert.match(block, /text-decoration-thickness:\s*2px;/); assert.match(block, /text-underline-offset:\s*4px;/); - assert.match(block, /color:\s*inherit;/); + assert.doesNotMatch(block, /(?:^|\n)\s*color\s*:/m); } for (let band = 1; band <= 5; band += 1) { @@ -446,6 +446,15 @@ test('JLPT CSS rules use underline-only styling in renderer stylesheet', () => { assert.match(coloredCharHoverBlock, /background:\s*transparent;/); assert.match(coloredCharHoverBlock, /color:\s*inherit\s*!important;/); + assert.match( + 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;/, + ); + assert.match( + 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;/, + ); + const selectionBlock = extractClassBlock(cssText, '#subtitleRoot::selection'); assert.match( selectionBlock,