package org.terst.feedreader.readability /** What to do with one `evaluateJavascript` result. */ sealed interface EvalDecision { /** Nothing more to try: this is the extraction's outcome. */ data class Done(val result: ExtractionResult) : EvalDecision /** The page probably was not ready; inject the script again. */ data object Retry : EvalDecision } /** * Decides, for one script result, whether extraction succeeded, should be * retried, or has failed and why. * * Lifted out of the WebView callback it used to live inside so it can be * exercised without a device. The bug that prompted that: a result consisting * only of the page's cookie-consent banner was non-blank and error-free, so it * counted as a success, was cached to `articles.extractedContent`, and was never * reconsidered. Reported live as "reader mode just extracts cookie disclaimer". */ internal fun decideEvalOutcome( article: ExtractedArticle?, rawResult: String, attempt: Int, maxAttempts: Int ): EvalDecision { val readabilityError = article?.error?.takeIf { it.isNotBlank() } // A banner is exactly what a half-loaded page produces -- the consent script // runs early, the article body may not be in the DOM yet -- so this is not // usable, which routes it into the same retry the extractor already does for // an empty result rather than being accepted. val isConsentNotice = article != null && readabilityError == null && ConsentNotice.looksLikeConsentNotice(article.content) val usable = article != null && readabilityError == null && !article.content.isNullOrBlank() && !isConsentNotice return when { // Cleaned on the way out rather than at the point of display: this is the one place // every fresh extraction passes through, so the widgets never reach the screen and // never reach `articles.extractedContent` either. usable -> EvalDecision.Done( ExtractionResult.Success( article!!.copy(content = RecirculationWidgets.strip(article.content)) ) ) // Readability ran and objected: that verdict will not change. readabilityError != null -> EvalDecision.Done(ExtractionResult.Failure(ExtractionFailure.ReadabilityError(readabilityError))) attempt < maxAttempts -> EvalDecision.Retry // Every attempt came back with nothing but the consent notice, so this is // what the page actually serves -- report it as its own failure rather // than passing a cookie disclaimer off as the article. isConsentNotice -> EvalDecision.Done(ExtractionResult.Failure(ExtractionFailure.ConsentInterstitial)) article == null -> EvalDecision.Done(ExtractionResult.Failure(ExtractionFailure.BadResult(rawResult))) else -> EvalDecision.Done(ExtractionResult.Failure(ExtractionFailure.NoContent)) } }