All Downloads are FREE. Search and download functionalities are using the official Maven repository.

unstatic.ztapir.ZTSite.scala Maven / Gradle / Ivy

package unstatic.ztapir

import scala.collection.*

import unstatic.*, UrlPath.*

import java.nio.file.{Files, Path as JPath}

import org.jsoup.Jsoup
import org.jsoup.nodes.Element

object ZTSite:
  trait Composite extends ZTSite:
    def endpointBindingSources : immutable.Seq[ZTEndpointBinding.Source]
    def endpointBindings : immutable.Seq[ZTEndpointBinding] = endpointBindingSources.flatMap( _.endpointBindings )
  end Composite

  trait SingleRootComposite( rootDir : JPath ) extends Composite:
    override val enforceUserContentFrom = Some(rootDir)

    private lazy val rootBinding = ZTEndpointBinding.staticDirectoryServing(Rooted.root, this, rootDir, immutable.Set("staticRoot"))
    override def endpointBindings : immutable.Seq[ZTEndpointBinding] = super.endpointBindings :+ rootBinding
  end SingleRootComposite

trait ZTSite extends Site, ZTEndpointBinding.Source, ExposesDuplicateIdentifiers:
  override def allBindings : immutable.Seq[AnyBinding] = this.endpointBindings

  val enforceUserContentFrom : Option[JPath]

  lazy val siteRootedPathByIdentifier =
    allBindings.reverse.flatMap( b => b.identifiers.toSeq.map(id => (id, b.siteRootedPath)) ).toMap

  private def siteRootedPathIsDefined( siteRootedPath : Rooted, binding : ZTEndpointBinding ) : Boolean =
    binding match
      case fsd : ZTEndpointBinding.FromStaticDirectory =>
        val file = fsd.dir.resolve(siteRootedPath.unroot.toString)
        Files.exists(file)
      case other =>
        siteRootedPath == other.siteRootedPath

  def siteRootedPathIsDefined( siteRootedPath : Rooted ) : Boolean =
    this.endpointBindings.exists( binding => siteRootedPathIsDefined(siteRootedPath, binding) )

  /**
   * a "master switch", should not generally be used (html outputs determine their own hash-special resolution policy),
   * but implemented so we can compare generation times, understand just how performance-costly the current Jsoup-based
   * resolution approach is.
   *
   * override this to true to disable all hash special resolution
   */
  val disableAllResolveHashSpecials = false

  def publicReadOnlyHtml(siteLocation: SiteLocation, task: zio.Task[String], mediaDirSiteRooted : Option[Rooted], identifiers : immutable.Set[String], resolveHashSpecials : Boolean, memoize : Boolean ) : ZTEndpointBinding =
    def resolvingTask : zio.Task[String] =
      val sourceSiteRooted = siteLocation.siteRootedPath
      task.map( rawHtml => htmlResolveHashSpecials(sourceSiteRooted.toString(), siteLocation.siteRootedPath, rawHtml, mediaDirSiteRooted, true ) )
    val base        : zio.Task[String] = if resolveHashSpecials then resolvingTask else task
    val mbMemoizing : zio.Task[String] = if memoize then base.memoize.flatten else base
    ZTEndpointBinding.publicReadOnlyHtml(siteLocation, mbMemoizing, mediaDirSiteRooted, identifiers )

  def htmlResolveHashSpecials( sourceId : String, sourceSiteRooted : Rooted, unresolvedHtml : String, mbMediaDirSiteRooted : Option[Rooted], resolveEscapes : Boolean ) : String =
    if disableAllResolveHashSpecials then
      unresolvedHtml
    else
      val jsoupDoc = org.jsoup.Jsoup.parse(unresolvedHtml)
      mutateHtmlResolveHashSpecials( jsoupDoc, sourceId, sourceSiteRooted, mbMediaDirSiteRooted, resolveEscapes )
      jsoupDoc.outerHtml()

  def htmlFragmentResolveHashSpecials( sourceId : String, sourceSiteRooted : Rooted, unresolvedHtml : String, mbMediaDirSiteRooted : Option[Rooted], resolveEscapes : Boolean ) : String =
    if disableAllResolveHashSpecials then
      unresolvedHtml
    else
      val jsoupDoc = org.jsoup.Jsoup.parseBodyFragment(unresolvedHtml)
      mutateHtmlResolveHashSpecials( jsoupDoc, sourceId, sourceSiteRooted, mbMediaDirSiteRooted, resolveEscapes )
      jsoupDoc.body().html

  private def mutateHtmlResolveHashSpecials( parentElem : Element, sourceId : String, sourceSiteRooted : Rooted, mbMediaDirSiteRooted : Option[Rooted], resolveEscapes : Boolean ) : Unit =
    def mutateReplace(cssQuery : String, refAttr : String) : Unit =
      import scala.jdk.CollectionConverters._
      parentElem.select(cssQuery).asScala.foreach { elem =>
        val rawHref = elem.attr(refAttr)
        val shinyHref = replaceMaybeHashSpecial(sourceId, sourceSiteRooted, rawHref, mbMediaDirSiteRooted, resolveEscapes)
        elem.attr(refAttr, shinyHref)
      }

    mutateReplace("a","href")
    mutateReplace("img","src")
    mutateReplace("link","href")

  private def replaceMaybeHashSpecial( sourceId : String, sourceSiteRooted : Rooted, href : String, mbMediaDirSiteRooted : Option[Rooted], resolveEscapes : Boolean ) : String =
    if href.startsWith("##") then
      val content = href.drop(2)
      if content.startsWith("/") then
        val destSiteRooted = Rooted(content)
        if !siteRootedPathIsDefined(destSiteRooted) then
          throw new UnresolvedReference(sourceId, href, s"For internal site reference, no endpoint handles destination path '${destSiteRooted}'")
        sourceSiteRooted.relativizeSibling(destSiteRooted).toString()
      else if content.startsWith("./") then
        mbMediaDirSiteRooted match
          case Some( mediaDirSiteRooted ) =>
            val relpath = Rel(content.drop(2))
            val destSiteRooted = mediaDirSiteRooted.resolve(relpath)
            enforceUserContentFrom.foreach { jpath =>
              val expectedFile = jpath.resolve(destSiteRooted.unroot.toString())
              if (!Files.exists(expectedFile))
                throw new UnresolvedReference(sourceId, href, s"For media-dir reference, expected destination file does not exist: '${expectedFile}'", Some(expectedFile.toAbsolutePath.toString))
            }
            sourceSiteRooted.relativizeSibling(destSiteRooted).toString()
          case None =>
            throw new UnresolvedReference(sourceId, href, s"Special hash reference '${href}' is relative to a mediaDir, but no mediaDir is available in this context.")
            //scribe.warn(s"${sourceId}: Special hash reference '${href}' is relative to a mediaDir, but no mediaDir is available in this context. Left as-is.")
            //href
      else if content(0) == '\\' then
        if resolveEscapes then // we only want to unescape at once, at a page's last pass, otherwise we might accidentally unescape to something later resolves
          "##" + content.drop(1) // lose one backslash
        else
          href
      else
        val id = content
        siteRootedPathByIdentifier.get(id) match
          case Some(path) => sourceSiteRooted.relativizeSibling(path).toString()
          case None =>
            throw new UnresolvedReference(sourceId, href, s"Refers to identifier '${id}', but that identifier is unknown to this site.")
            // scribe.warn(s"${sourceId}: Special hash reference '${href}' could not be interpreted or resolved to an identifier, left as-is.")
            // href
    else
      href




© 2015 - 2025 Weber Informatics LLC | Privacy Policy