@@ -452,7 +452,10 @@ export class ReaderView extends ItemView {
452452 }
453453 await this . displayPodcast ( item ) ;
454454 } else {
455- const fullContent = await this . fetchFullArticleContent ( item . link ) ;
455+ const fetchedContent = await this . fetchFullArticleContent ( item . link ) ;
456+ const fullContent = this . hasMeaningfulArticleContent ( fetchedContent )
457+ ? fetchedContent
458+ : item . content || item . description || "" ;
456459 this . currentFullContent = fullContent ;
457460 await this . displayArticle ( item , fullContent ) ;
458461 }
@@ -550,7 +553,12 @@ export class ReaderView extends ItemView {
550553 this . videoPlayer = null ;
551554 }
552555
553- if ( this . webViewerIntegration ) {
556+ const shouldUseWebViewer =
557+ Boolean ( this . settings . useWebViewer ) &&
558+ Boolean ( this . webViewerIntegration ) &&
559+ ! this . shouldBypassWebViewerForFeedContent ( item , fullContent ) ;
560+
561+ if ( shouldUseWebViewer && this . webViewerIntegration ) {
554562 try {
555563 const success = await this . webViewerIntegration . openInWebViewer (
556564 item . link ,
@@ -569,6 +577,27 @@ export class ReaderView extends ItemView {
569577 this . renderArticle ( item , fullContent ) ;
570578 }
571579
580+ private shouldBypassWebViewerForFeedContent (
581+ item : FeedItem ,
582+ fullContent ?: string ,
583+ ) : boolean {
584+ if ( ! item . link ) {
585+ return false ;
586+ }
587+
588+ const feedHtml = ( fullContent || item . content || item . description || "" ) . trim ( ) ;
589+ if ( ! feedHtml ) {
590+ return false ;
591+ }
592+
593+ try {
594+ const host = new URL ( item . link ) . hostname . toLowerCase ( ) ;
595+ return host === "kite.kagi.com" || host === "news.kagi.com" ;
596+ } catch {
597+ return false ;
598+ }
599+ }
600+
572601 private renderArticle ( item : FeedItem , fullContent ?: string ) : void {
573602 const headerContainer = this . readingContainer . createDiv ( {
574603 cls : "rss-reader-article-header" ,
@@ -615,42 +644,44 @@ export class ReaderView extends ItemView {
615644 }
616645 }
617646
618- if (
619- this . settings . display . showCoverImage &&
620- ( item . coverImage ||
621- ( item . image &&
622- typeof item . image === "object" &&
623- ( item . image as { url ?: string } ) . url ) ||
624- ( typeof item . image === "string" ? item . image : "" ) )
625- ) {
626- const imageContainer = this . readingContainer . createDiv ( {
627- cls : "rss-reader-cover-image" ,
628- } ) ;
629- const coverImg = imageContainer . createEl ( "img" , {
630- attr : {
631- src :
632- ( item . coverImage ||
633- ( item . image &&
634- typeof item . image === "object" &&
635- ( item . image as { url ?: string } ) . url ) ||
636- ( typeof item . image === "string" ? item . image : "" ) ) ??
637- "" ,
638- alt : item . title ,
639- } ,
647+ const descriptionHtml = ( item . description || "" ) . trim ( ) ;
648+ const mainHtml = ( fullContent || item . content || "" ) . trim ( ) ;
649+
650+ if ( descriptionHtml ) {
651+ const descriptionCallout = this . readingContainer . createEl ( "details" , {
652+ cls : "rss-reader-description-callout" ,
640653 } ) ;
641- coverImg . addEventListener ( "error" , function ( ) {
642- this . remove ( ) ;
654+ descriptionCallout . open = true ;
655+ descriptionCallout . createEl ( "summary" , { text : "Feed description" } ) ;
656+ const descriptionBody = descriptionCallout . createDiv ( {
657+ cls : "rss-reader-description rss-reader-description-body" ,
643658 } ) ;
659+ this . populateArticleHtml ( descriptionBody , descriptionHtml , item . link ) ;
644660 }
645661
646- const contentContainer = this . readingContainer . createDiv ( {
647- cls : "rss-reader-article-content" ,
648- } ) ;
662+ const hasDistinctMainContent =
663+ mainHtml && ( ! descriptionHtml || ! this . isEquivalentHtml ( mainHtml , descriptionHtml ) ) ;
649664
650- const htmlString = ensureUtf8Meta ( fullContent || item . description || "" ) ;
665+ if ( hasDistinctMainContent || ! descriptionHtml ) {
666+ const contentContainer = this . readingContainer . createDiv ( {
667+ cls : "rss-reader-article-content" ,
668+ } ) ;
669+ const htmlToRender = hasDistinctMainContent
670+ ? mainHtml
671+ : descriptionHtml || mainHtml ;
672+ this . populateArticleHtml ( contentContainer , htmlToRender , item . link ) ;
673+ }
674+ }
675+
676+ private populateArticleHtml (
677+ container : HTMLElement ,
678+ rawHtml : string ,
679+ baseUrl : string ,
680+ ) : void {
681+ const htmlString = ensureUtf8Meta ( rawHtml || "" ) ;
651682 const processedHtmlString = this . convertRelativeUrlsInContent (
652683 htmlString ,
653- item . link ,
684+ baseUrl ,
654685 ) ;
655686 const parser = new DOMParser ( ) ;
656687 const doc = parser . parseFromString ( processedHtmlString , "text/html" ) ;
@@ -661,7 +692,6 @@ export class ReaderView extends ItemView {
661692 parent . appendText ( node . textContent || "" ) ;
662693 } else if ( node . nodeType === Node . ELEMENT_NODE ) {
663694 const element = node as HTMLElement ;
664- // Skip icon elements that shouldn't be rendered
665695 const isIconElement =
666696 element . tagName === "I" && element . classList . contains ( "icon-class" ) ;
667697 if ( ! isIconElement ) {
@@ -679,9 +709,9 @@ export class ReaderView extends ItemView {
679709 } ) ;
680710 }
681711
682- appendNodes ( contentContainer , doc . body . childNodes ) ;
712+ appendNodes ( container , doc . body . childNodes ) ;
683713
684- contentContainer . querySelectorAll ( "img" ) . forEach ( ( img ) => {
714+ container . querySelectorAll ( "img" ) . forEach ( ( img ) => {
685715 const src = img . getAttribute ( "src" ) ;
686716 if ( src && src . startsWith ( "app://" ) ) {
687717 img . setAttribute ( "src" , src . replace ( "app://" , "https://" ) ) ;
@@ -693,7 +723,7 @@ export class ReaderView extends ItemView {
693723 } ) ;
694724 } ) ;
695725
696- contentContainer . querySelectorAll ( "source" ) . forEach ( ( source ) => {
726+ container . querySelectorAll ( "source" ) . forEach ( ( source ) => {
697727 const srcset = source . getAttribute ( "srcset" ) ;
698728 if ( srcset ) {
699729 const processedSrcset = srcset
@@ -721,28 +751,47 @@ export class ReaderView extends ItemView {
721751 }
722752 } ) ;
723753
724- contentContainer . querySelectorAll ( "a" ) . forEach ( ( link ) => {
754+ container . querySelectorAll ( "a" ) . forEach ( ( link ) => {
725755 const href = link . getAttribute ( "href" ) ;
726756 if ( href && href . startsWith ( "app://" ) ) {
727757 link . setAttribute ( "href" , href . replace ( "app://" , "https://" ) ) ;
728758 }
729- } ) ;
730-
731- this . app . workspace . trigger ( "parse-math" , contentContainer ) ;
732-
733- const links = contentContainer . querySelectorAll ( "a" ) ;
734- links . forEach ( ( link ) => {
735759 link . setAttribute ( "target" , "_blank" ) ;
736760 link . setAttribute ( "rel" , "noopener noreferrer" ) ;
737761 } ) ;
738762
739- // Apply word highlighting to content if enabled
763+ this . app . workspace . trigger ( "parse-math" , container ) ;
764+
740765 if (
741766 this . settings . highlights ?. enabled &&
742767 this . settings . highlights . highlightInContent
743768 ) {
744769 const highlightService = new HighlightService ( this . settings . highlights ) ;
745- highlightService . highlightElement ( contentContainer ) ;
770+ highlightService . highlightElement ( container ) ;
771+ }
772+ }
773+
774+ private isEquivalentHtml ( first : string , second : string ) : boolean {
775+ if ( ! first || ! second ) {
776+ return false ;
777+ }
778+
779+ try {
780+ const parser = new DOMParser ( ) ;
781+ const firstDoc = parser . parseFromString ( ensureUtf8Meta ( first ) , "text/html" ) ;
782+ const secondDoc = parser . parseFromString (
783+ ensureUtf8Meta ( second ) ,
784+ "text/html" ,
785+ ) ;
786+ const firstText = ( firstDoc . body . textContent || "" )
787+ . replace ( / \s + / g, " " )
788+ . trim ( ) ;
789+ const secondText = ( secondDoc . body . textContent || "" )
790+ . replace ( / \s + / g, " " )
791+ . trim ( ) ;
792+ return firstText . length > 0 && firstText === secondText ;
793+ } catch {
794+ return first . trim ( ) === second . trim ( ) ;
746795 }
747796 }
748797
@@ -761,6 +810,36 @@ export class ReaderView extends ItemView {
761810 }
762811 }
763812
813+ private hasMeaningfulArticleContent ( content : string ) : boolean {
814+ if ( ! content || ! content . trim ( ) ) {
815+ return false ;
816+ }
817+
818+ try {
819+ const parser = new DOMParser ( ) ;
820+ const doc = parser . parseFromString ( content , "text/html" ) ;
821+ const body = doc . body ;
822+ if ( ! body ) {
823+ return false ;
824+ }
825+
826+ body . querySelectorAll ( "script, style, noscript" ) . forEach ( ( node ) => {
827+ node . remove ( ) ;
828+ } ) ;
829+
830+ const text = ( body . textContent || "" ) . replace ( / \s + / g, " " ) . trim ( ) ;
831+ if ( text . length >= 80 ) {
832+ return true ;
833+ }
834+
835+ return Boolean (
836+ body . querySelector ( "p, article, li, blockquote, h1, h2, h3, h4" ) ,
837+ ) ;
838+ } catch {
839+ return content . trim ( ) . length >= 80 ;
840+ }
841+ }
842+
764843 private convertHtmlToMarkdown ( html : string ) : string {
765844 return this . turndownService . turndown ( html ) ;
766845 }
0 commit comments