@@ -484,4 +484,207 @@ class XQTTest {
484484 assertTrue(html.contains(mediaUrl))
485485 assertTrue(html.contains(" Text after media." ))
486486 }
487+
488+ @Test
489+ fun renderContent_complexEntities_rendersCorrectly () {
490+ // Text: "Start 🫠 @user End"
491+ // Indices (UTF-16):
492+ // "Start " -> 0..5 (6 chars)
493+ // "🫠" -> 6..7 (2 chars, 1 code point)
494+ // " " -> 8 (1 char)
495+ // "@user" -> 9..13 (5 chars)
496+ // " " -> 14 (1 char)
497+ // "End" -> 15..17 (3 chars)
498+ val text = " Start 🫠 @user End"
499+
500+ val mediaId = " media_123"
501+ val mediaUrl = " https://example.com/image.jpg"
502+
503+ val noteTweet =
504+ NoteTweet (
505+ isExpandable = true ,
506+ noteTweetResults =
507+ NoteTweetResult (
508+ result =
509+ NoteTweetResultData (
510+ id = " note_id" ,
511+ text = text,
512+ entitySet =
513+ Entities (
514+ userMentions =
515+ listOf (
516+ UserMention (
517+ screenName = " user" ,
518+ name = " User" ,
519+ indices = listOf (8 , 13 ),
520+ ),
521+ ),
522+ media =
523+ listOf (
524+ dev.dimension.flare.data.network.xqt.model.Media (
525+ idStr = mediaId,
526+ mediaUrlHttps = mediaUrl,
527+ type = dev.dimension.flare.data.network.xqt.model.Media .Type .photo,
528+ originalInfo =
529+ dev.dimension.flare.data.network.xqt.model
530+ .MediaOriginalInfo (100 , 100 ),
531+ displayUrl = " example.com/image" ,
532+ expandedUrl = mediaUrl,
533+ url = mediaUrl,
534+ indices = listOf (0 , 5 ), // dummy
535+ sizes =
536+ dev.dimension.flare.data.network.xqt.model.MediaSizes (
537+ large =
538+ dev.dimension.flare.data.network.xqt.model.MediaSize (
539+ 100 ,
540+ dev.dimension.flare.data.network.xqt.model.MediaSize .Resize .crop,
541+ 100 ,
542+ ),
543+ medium =
544+ dev.dimension.flare.data.network.xqt.model.MediaSize (
545+ 100 ,
546+ dev.dimension.flare.data.network.xqt.model.MediaSize .Resize .crop,
547+ 100 ,
548+ ),
549+ small =
550+ dev.dimension.flare.data.network.xqt.model.MediaSize (
551+ 100 ,
552+ dev.dimension.flare.data.network.xqt.model.MediaSize .Resize .crop,
553+ 100 ,
554+ ),
555+ thumb =
556+ dev.dimension.flare.data.network.xqt.model.MediaSize (
557+ 100 ,
558+ dev.dimension.flare.data.network.xqt.model.MediaSize .Resize .crop,
559+ 100 ,
560+ ),
561+ ),
562+ ),
563+ ),
564+ ),
565+ media =
566+ dev.dimension.flare.data.network.xqt.model.NoteTweetResultMedia (
567+ inlineMedia =
568+ listOf (
569+ dev.dimension.flare.data.network.xqt.model.NoteTweetResultMediaInlineMedia (
570+ index = 14 , // UTF-16 index before " " (Space before End)
571+ mediaId = mediaId,
572+ ),
573+ ),
574+ ),
575+ richtext =
576+ NoteTweetResultRichText (
577+ richtextTags =
578+ listOf (
579+ NoteTweetResultRichTextTag (
580+ fromIndex = 0 ,
581+ toIndex = 5 ,
582+ richtextTypes = listOf (NoteTweetResultRichTextTag .RichtextTypes .bold),
583+ ),
584+ NoteTweetResultRichTextTag (
585+ fromIndex = 15 ,
586+ toIndex = 18 ,
587+ richtextTypes = listOf (NoteTweetResultRichTextTag .RichtextTypes .italic),
588+ ),
589+ ),
590+ ),
591+ ),
592+ ),
593+ )
594+ // Legacy needed for resolving media URL in inline media processing
595+ val legacy =
596+ TweetLegacy (
597+ idStr = " 123" ,
598+ fullText = " Legacy text" ,
599+ displayTextRange = listOf (0 , 10 ),
600+ createdAt = " Wed Oct 10 20:19:24 +0000 2018" ,
601+ favoriteCount = 0 ,
602+ favorited = false ,
603+ isQuoteStatus = false ,
604+ lang = " en" ,
605+ quoteCount = 0 ,
606+ replyCount = 0 ,
607+ retweetCount = 0 ,
608+ retweeted = false ,
609+ entities =
610+ Entities (
611+ media =
612+ listOf (
613+ dev.dimension.flare.data.network.xqt.model.Media (
614+ idStr = mediaId,
615+ mediaUrlHttps = mediaUrl,
616+ type = dev.dimension.flare.data.network.xqt.model.Media .Type .photo,
617+ originalInfo =
618+ dev.dimension.flare.data.network.xqt.model
619+ .MediaOriginalInfo (100 , 100 ),
620+ displayUrl = " example.com/image" ,
621+ expandedUrl = mediaUrl,
622+ url = mediaUrl,
623+ indices = listOf (0 , 5 ),
624+ sizes =
625+ dev.dimension.flare.data.network.xqt.model.MediaSizes (
626+ large =
627+ dev.dimension.flare.data.network.xqt.model.MediaSize (
628+ 100 ,
629+ dev.dimension.flare.data.network.xqt.model.MediaSize .Resize .crop,
630+ 100 ,
631+ ),
632+ medium =
633+ dev.dimension.flare.data.network.xqt.model.MediaSize (
634+ 100 ,
635+ dev.dimension.flare.data.network.xqt.model.MediaSize .Resize .crop,
636+ 100 ,
637+ ),
638+ small =
639+ dev.dimension.flare.data.network.xqt.model.MediaSize (
640+ 100 ,
641+ dev.dimension.flare.data.network.xqt.model.MediaSize .Resize .crop,
642+ 100 ,
643+ ),
644+ thumb =
645+ dev.dimension.flare.data.network.xqt.model.MediaSize (
646+ 100 ,
647+ dev.dimension.flare.data.network.xqt.model.MediaSize .Resize .crop,
648+ 100 ,
649+ ),
650+ ),
651+ ),
652+ ),
653+ ),
654+ )
655+
656+ val tweet =
657+ Tweet (
658+ restId = " 123" ,
659+ noteTweet = noteTweet,
660+ legacy = legacy,
661+ )
662+
663+ val result = tweet.renderContent(accountKey)
664+ val html = result.html
665+
666+ // Expected Structure: "Start 🫠 <a ...>@user</a> <figure>...</figure>End"
667+
668+ // Expected Structure: "<b>Start</b> 🫠 <a ...>@user</a> <figure>...</figure><i>End</i>"
669+
670+ assertTrue(html.contains(" <b>Start</b>" ), " Start should be bold" )
671+ assertTrue(html.contains(" 🫠" ), " Emoji preserved" )
672+ assertTrue(html.contains(" >@user</a>" ), " User mention linked" )
673+ assertTrue(html.contains(" <figure>" ), " Media figure present" )
674+ assertTrue(html.contains(" src=\" $mediaUrl \" " ), " Media source correct" )
675+ assertTrue(html.contains(" <i>End</i>" ), " End should be italic" )
676+
677+ // order check
678+ val indexUser = html.indexOf(" >@user</a>" )
679+ val indexMedia = html.indexOf(" <figure>" )
680+ val indexEnd = html.indexOf(" End" )
681+
682+ assertTrue(indexUser != - 1 )
683+ assertTrue(indexMedia != - 1 )
684+ assertTrue(indexEnd != - 1 )
685+
686+ assertTrue(indexUser < indexMedia, " User mention should be before media" )
687+ assertTrue(indexUser < indexMedia, " User mention should be before media" )
688+ assertTrue(indexMedia < indexEnd, " Media should be before 'End'" )
689+ }
487690}
0 commit comments