common/app/views/support/package.scala (178 lines of code) (raw):
package views.support
import java.text.DecimalFormat
import common._
import model.Cached.WithoutRevalidationResult
import model._
import model.pressed.PressedContent
import org.apache.commons.lang.StringEscapeUtils
import org.joda.time.format.DateTimeFormat
import org.joda.time.{DateTime, DateTimeZone, LocalDate}
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.safety.{Cleaner, Safelist}
import play.api.libs.json.Json._
import play.api.libs.json.Writes
import play.api.mvc.{RequestHeader, Result}
import play.twirl.api.Html
import layout.slices.ContainerDefinition
import scala.jdk.CollectionConverters._
/** Encapsulates previous and next urls
*/
case class PreviousAndNext(prev: Option[String], next: Option[String]) {
val isDefined: Boolean = prev.isDefined || next.isDefined
}
object JSON {
// we wrap the result in an Html so that play does not escape it as html
// after we have gone to the trouble of escaping it as Javascript
def apply[T](json: T)(implicit tjs: Writes[T]): Html = Html(stringify(toJson(json)))
}
//annoyingly content api will sometimes have things surrounded by <p> tags and sometimes not.
//since you cannot nest <p> tags this causes all sorts of problems
object RemoveOuterParaHtml {
def apply(html: Html): Html = this(html.body)
def apply(text: String): Html = {
val fragment = Jsoup.parseBodyFragment(text).body()
if (!fragment.html().startsWith("<p>")) {
Html(text)
} else {
Html(fragment.html.drop(3).dropRight(4))
}
}
}
case class RowInfo(rowNum: Int, isLast: Boolean = false) {
lazy val isFirst = rowNum == 1
lazy val isEven = rowNum % 2 == 0
lazy val isOdd = !isEven
lazy val rowClass = rowNum match {
case 1 => s"first ${_rowClass}"
case _ if isLast => s"last ${_rowClass}"
case _ => _rowClass
}
private lazy val _rowClass = if (isEven) "even" else "odd"
def indexIsInSameCarousel(carouselWidth: Int, candidate: Int): Boolean = {
val tolerance = (carouselWidth - 1) / 2 // Your problem if carouselWidth % 2 == 0
val carousel = (rowNum - tolerance) to (rowNum + tolerance)
carousel contains candidate
}
}
// whitespace in the <span> below is significant
// (results in spaces after author names before commas)
// so don't add any, fool.
object ContributorLinks {
/*
The removeDuplicateNames was introduced to correct a bug in the original workings of the apply's fold, by which
The presence of more than one tag with a given name ( `tag.name` ) would result in nested HTML markups.
This was never noticed before because the anchor at the most inner position of the nesting wold be displayed correctly.
Original output of the fold:
<span itemscope="" itemtype="http://schema.org/Person" itemprop="author">
<a rel="author" class="tone-colour" itemprop="sameAs" data-link-name="auto tag link" href="http://localhost:9000/profile/scottbryan">
<span itemprop="name">
<span itemscope="" itemtype="http://schema.org/Person" itemprop="author">
<a rel="author" class="tone-colour" itemprop="sameAs" data-link-name="auto tag link" href="http://localhost:9000/profile/scott-bryan">
<span itemprop="name">
<span itemscope="" itemtype="http://schema.org/Person" itemprop="author">
<a rel="author" class="tone-colour" itemprop="sameAs" data-link-name="auto tag link" href="http://localhost:9000/profile/scottbryan">
<span itemprop="name">Scott Bryan</span>
</a>
</span>
</span>
</a>
</span>
</span>
</a>
</span>
and
<span itemscope="" itemtype="http://schema.org/Person" itemprop="author">
<a rel="author" class="tone-colour" itemprop="sameAs" data-link-name="auto tag link" href="http://localhost:9000/profile/michael-chakraverty">
<span itemprop="name">Michael Chakraverty</span>
</a>
</span>
Updated/Corrected output of the fold:
<span itemscope="" itemtype="http://schema.org/Person" itemprop="author">
<a rel="author" class="tone-colour" itemprop="sameAs" data-link-name="auto tag link" href="http://localhost:9000/profile/scottbryan">
<span itemprop="name">Scott Bryan</span>
</a>
</span>
and
<span itemscope="" itemtype="http://schema.org/Person" itemprop="author">
<a rel="author" class="tone-colour" itemprop="sameAs" data-link-name="auto tag link" href="http://localhost:9000/profile/michael-chakraverty">
<span itemprop="name">Michael Chakraverty</span>
</a>
</span>
*/
def removeDuplicateNames(tags: Seq[Tag]): Seq[Tag] = tags.groupBy(_.name).map(_._2.head).toSeq
def apply(text: String, tags: Seq[Tag])(implicit request: RequestHeader): Html = {
Html {
removeDuplicateNames(tags).foldLeft(text) { case (t, tag) =>
t.replaceFirst(
tag.name,
s"""<span itemscope="" itemtype="http://schema.org/Person" itemprop="author">
| <a rel="author" class="tone-colour" itemprop="sameAs" data-link-name="auto tag link"
| href="${LinkTo("/" + tag.id)}"><span itemprop="name">${tag.name}</span></a></span>""".stripMargin,
)
}
}
}
def apply(html: Html, tags: Seq[Tag])(implicit request: RequestHeader): Html = apply(html.body, tags)
}
object `package` {
def withJsoup(html: Html)(cleaners: HtmlCleaner*): Html = withJsoup(html.body)(cleaners: _*)
def withJsoup(html: String)(cleaners: HtmlCleaner*): Html = {
val cleanedHtml = cleaners.foldLeft(Jsoup.parseBodyFragment(html)) { case (html, cleaner) => cleaner.clean(html) }
Html(cleanedHtml.body.html)
}
def getTagContainerDefinition(page: ContentPage): ContainerDefinition = {
if (page.item.tags.isContributorPage) {
layout.slices.TagContainers.contributorTagPage
} else if (page.item.tags.keywords.nonEmpty) {
layout.slices.TagContainers.keywordPage
} else {
layout.slices.TagContainers.tagPage
}
}
implicit class Tags2tagUtils(t: Tags) {
def typeOrTone: Option[Tag] = t.types.find(_.id != "type/article").orElse(t.tones.headOption)
}
implicit class Seq2zipWithRowInfo[A](seq: Seq[A]) {
def zipWithRowInfo: Seq[(A, RowInfo)] =
seq.zipWithIndex.map { case (item, index) =>
(item, RowInfo(index + 1, seq.length == index + 1))
}
}
}
object GuDateFormatLegacy {
def apply(date: DateTime, pattern: String, tzOverride: Option[DateTimeZone] = None)(implicit
request: RequestHeader,
): String = {
apply(date, Edition(request), pattern, tzOverride)
}
def apply(date: DateTime, edition: Edition, pattern: String, tzOverride: Option[DateTimeZone]): String = {
val timeZone = tzOverride match {
case Some(tz) => tz
case _ => edition.timezone
}
date.toString(DateTimeFormat.forPattern(pattern).withZone(timeZone))
}
def apply(date: LocalDate, pattern: String)(implicit request: RequestHeader): String =
this(date.toDateTimeAtStartOfDay, pattern)(request)
def apply(a: Int): String = new DecimalFormat("#,###").format(a)
}
object cleanTrailText {
def apply(text: String)(implicit edition: Edition, request: RequestHeader): Html = {
withJsoup(RemoveOuterParaHtml(BulletCleaner(text)))(InBodyLinkCleaner("in trail text link"))
}
}
object StripHtmlTags {
def apply(html: String): String = Jsoup.clean(html, "", Safelist.none())
}
object StripHtmlTagsAndUnescapeEntities {
def apply(html: String): String = {
val doc = new Cleaner(Safelist.none()).clean(Jsoup.parse(html))
val stripped = doc.body.html
val unescaped = StringEscapeUtils.unescapeHtml(stripped)
unescaped.replace("\"", """) // double quotes will break HTML attributes
}
}
object TableEmbedComplimentaryToP extends HtmlCleaner {
override def clean(document: Document): Document = {
document.getElementsByClass("element-table").asScala.foreach { element =>
Option(element.nextElementSibling).map { nextSibling =>
if (nextSibling.tagName == "p") element.addClass("element-table--complimentary")
}
}
document
}
}
object RenderOtherStatus {
def gonePage(implicit request: RequestHeader): SimplePage = {
val canonicalUrl: Option[String] = Some(s"/${request.path.drop(1).split("/").head}")
SimplePage(
MetaData.make(
id = request.path,
section = Some(SectionId.fromId("news")),
webTitle = "This page has been removed",
canonicalUrl,
contentType = Some(model.DotcomContentType.Unknown),
),
)
}
def apply(result: Result)(implicit request: RequestHeader, context: ApplicationContext): Result =
result.header.status match {
case 404 => NoCache(NotFound)
case 410 if request.isJson => Cached(60)(JsonComponent(gonePage, "status" -> "GONE"))
case 410 =>
Cached(60)(
WithoutRevalidationResult(
Gone(
views.html.gone(
gonePage,
"Sorry - this page has been removed.",
"This could be, for example, because content associated with it is not yet published, or due to legal reasons such as the expiry of our rights to publish the content.",
),
),
),
)
case _ => result
}
}
object RenderClasses {
def apply(classes: Map[String, Boolean], extraClasses: String*): String =
apply((classes.filter(_._2).keys ++ extraClasses).toSeq: _*)
def apply(classes: String*): String = classes.filter(_.nonEmpty).sorted.distinct.mkString(" ")
}
object SnapData {
def apply(faciaContent: PressedContent): String = generateDataAttributes(faciaContent).mkString(" ")
private def generateDataAttributes(faciaContent: PressedContent): Iterable[String] =
faciaContent.properties.embedType.filter(_.nonEmpty).map(t => s"data-snap-type=$t") ++
faciaContent.properties.embedUri.filter(_.nonEmpty).map(t => s"data-snap-uri=$t")
}