@@ -265,6 +265,10 @@ const KaraokeLine = react.memo(({ line, position, isActive, globalCharOffset = 0
265265 const processedText = Utils . applyFuriganaIfEnabled ( rawLineText ) ;
266266 const hasFurigana = processedText !== rawLineText && processedText . includes ( '<ruby>' ) ;
267267
268+ console . log ( '[Karaoke Line Debug] rawLineText:' , rawLineText ) ;
269+ console . log ( '[Karaoke Line Debug] processedText:' , processedText ) ;
270+ console . log ( '[Karaoke Line Debug] hasFurigana:' , hasFurigana ) ;
271+
268272 // 전체 글자 정보를 먼저 수집
269273 const allChars = [ ] ;
270274 line . syllables . forEach ( ( syllable , syllableIndex ) => {
@@ -364,6 +368,9 @@ const KaraokeLine = react.memo(({ line, position, isActive, globalCharOffset = 0
364368 // Build text from charRenderData to ensure exact matching
365369 const actualText = charRenderData . map ( c => c . char ) . join ( '' ) ;
366370
371+ console . log ( '[Karaoke Line Debug] actualText from charRenderData:' , actualText ) ;
372+ console . log ( '[Karaoke Line Debug] charRenderData length:' , charRenderData . length ) ;
373+
367374 // Reset regex state
368375 whitespacePattern . lastIndex = 0 ;
369376 while ( ( match = whitespacePattern . exec ( actualText ) ) !== null ) {
@@ -383,9 +390,69 @@ const KaraokeLine = react.memo(({ line, position, isActive, globalCharOffset = 0
383390 const totalTokenChars = tokens . reduce ( ( sum , token ) => sum + Array . from ( token . value ) . length , 0 ) ;
384391 const actualCharCount = charRenderData . length ;
385392
393+ // Parse furigana HTML to extract readings for each kanji with position tracking
394+ // Do this BEFORE word grouping so it's available for both paths
395+ const furiganaMap = new Map ( ) ; // position -> reading
396+ if ( hasFurigana ) {
397+ const rubyRegex = / < r u b y > ( [ ^ < ] + ) < r t > ( [ ^ < ] + ) < \/ r t > < \/ r u b y > / g;
398+
399+ // Build clean text from processedText (removing all HTML tags)
400+ const cleanText = processedText . replace ( / < r u b y > ( [ ^ < ] + ) < r t > [ ^ < ] + < \/ r t > < \/ r u b y > / g, '$1' ) ;
401+
402+ // Now parse the HTML and map positions
403+ let currentPos = 0 ;
404+ let lastMatchEnd = 0 ;
405+ let match ;
406+
407+ rubyRegex . lastIndex = 0 ;
408+
409+ while ( ( match = rubyRegex . exec ( processedText ) ) !== null ) {
410+ const kanjiSequence = match [ 1 ] ;
411+ const reading = match [ 2 ] ;
412+
413+ // Calculate position by counting plain text before this match
414+ const beforeMatch = processedText . substring ( lastMatchEnd , match . index ) ;
415+ const plainTextBefore = beforeMatch . replace ( / < [ ^ > ] + > / g, '' ) ;
416+ currentPos += plainTextBefore . length ;
417+
418+ // Map each kanji to its reading
419+ if ( kanjiSequence . length === 1 ) {
420+ furiganaMap . set ( currentPos , reading ) ;
421+ } else {
422+ // Multiple kanji - split the reading
423+ const kanjiChars = Array . from ( kanjiSequence ) ;
424+ const readingChars = Array . from ( reading ) ;
425+ const charsPerKanji = Math . floor ( readingChars . length / kanjiChars . length ) ;
426+
427+ kanjiChars . forEach ( ( kanji , idx ) => {
428+ let kanjiReading ;
429+ if ( idx === kanjiChars . length - 1 ) {
430+ // Last kanji gets all remaining reading
431+ kanjiReading = readingChars . slice ( idx * charsPerKanji ) . join ( '' ) ;
432+ } else {
433+ kanjiReading = readingChars . slice ( idx * charsPerKanji , ( idx + 1 ) * charsPerKanji ) . join ( '' ) ;
434+ }
435+ furiganaMap . set ( currentPos + idx , kanjiReading ) ;
436+ } ) ;
437+ }
438+
439+ // Move position forward by the number of kanji
440+ currentPos += kanjiSequence . length ;
441+ lastMatchEnd = match . index + match [ 0 ] . length ;
442+ }
443+
444+ console . log ( '[Karaoke Furigana Debug] furiganaMap:' , Array . from ( furiganaMap . entries ( ) ) ) ;
445+ console . log ( '[Karaoke Furigana Debug] actualText:' , actualText ) ;
446+ console . log ( '[Karaoke Furigana Debug] cleanText:' , cleanText ) ;
447+ }
448+
386449 // Word grouping works if we have spaces and total chars match
387450 let useWordGrouping = hasWhitespaceToken && totalTokenChars === actualCharCount ;
388451
452+ console . log ( '[Karaoke Word Grouping] hasWhitespaceToken:' , hasWhitespaceToken ) ;
453+ console . log ( '[Karaoke Word Grouping] totalTokenChars:' , totalTokenChars , 'actualCharCount:' , actualCharCount ) ;
454+ console . log ( '[Karaoke Word Grouping] useWordGrouping:' , useWordGrouping ) ;
455+
389456 if ( useWordGrouping ) {
390457 let charCursor = 0 ;
391458 let mappingFailed = false ;
@@ -404,17 +471,41 @@ const KaraokeLine = react.memo(({ line, position, isActive, globalCharOffset = 0
404471 return ;
405472 }
406473
407- const wordChildren = wordCharData . map ( charData =>
408- react . createElement (
409- "span" ,
410- {
411- key : charData . key ,
412- className : charData . className ,
413- style : charData . style
414- } ,
415- charData . char
416- )
417- ) ;
474+ // Apply furigana to each character in the word
475+ const wordChildren = wordCharData . map ( ( charData , localIdx ) => {
476+ const globalIdx = charCursor + localIdx ;
477+ const char = charData . char ;
478+ const reading = furiganaMap . get ( globalIdx ) ;
479+
480+ if ( reading ) {
481+ // Has furigana
482+ return react . createElement (
483+ "span" ,
484+ {
485+ key : charData . key ,
486+ className : charData . className ,
487+ style : charData . style
488+ } ,
489+ react . createElement (
490+ "ruby" ,
491+ null ,
492+ char ,
493+ react . createElement ( "rt" , null , reading )
494+ )
495+ ) ;
496+ } else {
497+ // No furigana
498+ return react . createElement (
499+ "span" ,
500+ {
501+ key : charData . key ,
502+ className : charData . className ,
503+ style : charData . style
504+ } ,
505+ char
506+ ) ;
507+ }
508+ } ) ;
418509
419510 elements . push (
420511 react . createElement (
@@ -472,42 +563,14 @@ const KaraokeLine = react.memo(({ line, position, isActive, globalCharOffset = 0
472563 }
473564
474565 if ( ! useWordGrouping ) {
475- // Parse furigana HTML to extract readings for each kanji
476- const furiganaMap = new Map ( ) ;
477- if ( hasFurigana ) {
478- // Extract kanji and their readings from processedText
479- const rubyRegex = / < r u b y > ( [ ^ < ] + ) < r t > ( [ ^ < ] + ) < \/ r t > < \/ r u b y > / g;
480- let match ;
481- while ( ( match = rubyRegex . exec ( processedText ) ) !== null ) {
482- const kanjiSequence = match [ 1 ] ;
483- const reading = match [ 2 ] ;
484-
485- // If it's a single character, just map it directly
486- if ( kanjiSequence . length === 1 ) {
487- furiganaMap . set ( kanjiSequence , reading ) ;
488- } else {
489- // Multiple kanji - split the reading evenly
490- const kanjiChars = Array . from ( kanjiSequence ) ;
491- const readingChars = Array . from ( reading ) ;
492- const charsPerKanji = Math . floor ( readingChars . length / kanjiChars . length ) ;
493-
494- kanjiChars . forEach ( ( kanji , idx ) => {
495- if ( idx === kanjiChars . length - 1 ) {
496- // Last kanji gets all remaining reading
497- const kanjiReading = readingChars . slice ( idx * charsPerKanji ) . join ( '' ) ;
498- furiganaMap . set ( kanji , kanjiReading ) ;
499- } else {
500- const kanjiReading = readingChars . slice ( idx * charsPerKanji , ( idx + 1 ) * charsPerKanji ) . join ( '' ) ;
501- furiganaMap . set ( kanji , kanjiReading ) ;
502- }
503- } ) ;
504- }
505- }
506- }
507-
566+ // Furigana map is already built above, just use it
508567 charRenderData . forEach ( ( charData , index ) => {
509568 const char = charData . char ;
510- const reading = furiganaMap . get ( char ) ;
569+ const reading = furiganaMap . get ( index ) ; // Use position index instead of character
570+
571+ if ( index < 10 ) { // Log first 10 chars for debugging
572+ console . log ( `[Karaoke Furigana] Char[${ index } ]: '${ char } ', reading: '${ reading } '` ) ;
573+ }
511574
512575 // If this character has a furigana reading, wrap in ruby tag
513576 if ( reading ) {
0 commit comments