diff --git a/preview/src/main/scala/laika/preview/ASTPageTransformer.scala b/preview/src/main/scala/laika/preview/ASTPageTransformer.scala index 6448bcdd7..19dd0c6f5 100644 --- a/preview/src/main/scala/laika/preview/ASTPageTransformer.scala +++ b/preview/src/main/scala/laika/preview/ASTPageTransformer.scala @@ -25,6 +25,7 @@ import laika.ast.{ CodeBlock, Document, DocumentTreeRoot, + Path, RewritePhase, RootElement, Section, @@ -47,14 +48,16 @@ private[preview] object ASTPageTransformer { val description: String = "AST URL extension for preview server" private val outputName = "ast" + def translateASTPath(path: Path): Path = { + val base = path.withoutFragment / outputName + path.fragment.fold(base)(base.withFragment) + } + override def extendPathTranslator : PartialFunction[ExtensionBundle.PathTranslatorExtensionContext, PathTranslator] = { case context => PathTranslator.postTranslate(context.baseTranslator) { path => - if (path.suffix.contains("html")) { - val base = path.withoutFragment / outputName - path.fragment.fold(base)(base.withFragment) - } + if (path.suffix.contains("html")) translateASTPath(path) else path } } diff --git a/preview/src/main/scala/laika/preview/RouteBuilder.scala b/preview/src/main/scala/laika/preview/RouteBuilder.scala index 4442d0366..eb952bfbb 100644 --- a/preview/src/main/scala/laika/preview/RouteBuilder.scala +++ b/preview/src/main/scala/laika/preview/RouteBuilder.scala @@ -17,14 +17,21 @@ package laika.preview import java.io.InputStream - import cats.syntax.all._ import cats.effect.{ Async, Resource, Sync } import fs2.concurrent.Topic import fs2.io.readInputStream import laika.preview.ServerBuilder.Logger import org.http4s.dsl.Http4sDsl -import org.http4s.{ CacheDirective, EntityEncoder, Headers, HttpRoutes, MediaType, ServerSentEvent } +import org.http4s.{ + CacheDirective, + EntityEncoder, + Headers, + HttpRoutes, + MediaType, + Response, + ServerSentEvent +} import org.http4s.headers.{ `Cache-Control`, `Content-Type` } import scala.concurrent.duration.DurationInt @@ -48,6 +55,25 @@ private[preview] class RouteBuilder[F[_]: Async]( private val noCache = `Cache-Control`(CacheDirective.`no-store`) + def serve(laikaPath: laika.ast.Path, result: Option[SiteResult[F]]): F[Response[F]] = + result match { + case Some(RenderedResult(content)) => + logger(s"serving path $laikaPath - transformed markup") *> + Ok(content).map( + _ + .withHeaders(noCache) + .withContentType(`Content-Type`(MediaType.text.html)) + ) + case Some(StaticResult(input)) => + logger(s"serving path $laikaPath - static input") *> { + val mediaType = laikaPath.suffix.flatMap(mediaTypeFor).map(`Content-Type`(_)) + Ok(input).map(_.withHeaders(Headers(mediaType, noCache))) + } + case Some(LazyResult(res)) => res.flatMap(serve(laikaPath, _)) + case None => + logger(s"serving path $laikaPath - not found") *> NotFound() + } + def build: HttpRoutes[F] = HttpRoutes.of[F] { case GET -> Root / "laika" / "events" => @@ -56,22 +82,7 @@ private[preview] class RouteBuilder[F[_]: Async]( case GET -> path => val laikaPath = laika.ast.Path.parse(path.toString) - cache.get.map(_.get(laikaPath.withoutFragment)).flatMap { - case Some(RenderedResult(content)) => - logger(s"serving path $laikaPath - transformed markup") *> - Ok(content).map( - _ - .withHeaders(noCache) - .withContentType(`Content-Type`(MediaType.text.html)) - ) - case Some(StaticResult(input)) => - logger(s"serving path $laikaPath - static input") *> { - val mediaType = laikaPath.suffix.flatMap(mediaTypeFor).map(`Content-Type`(_)) - Ok(input).map(_.withHeaders(Headers(mediaType, noCache))) - } - case None => - logger(s"serving path $laikaPath - not found") *> NotFound() - } + cache.get.map(_.get(laikaPath.withoutFragment)).flatMap(serve(laikaPath, _)) } } diff --git a/preview/src/main/scala/laika/preview/SiteTransformer.scala b/preview/src/main/scala/laika/preview/SiteTransformer.scala index d01efaaf9..341168536 100644 --- a/preview/src/main/scala/laika/preview/SiteTransformer.scala +++ b/preview/src/main/scala/laika/preview/SiteTransformer.scala @@ -18,6 +18,7 @@ package laika.preview import cats.effect.{ Async, Resource } import cats.syntax.all._ +import cats.effect.syntax.all._ import fs2.Chunk import laika.api.Renderer import laika.api.builder.OperationConfig @@ -98,13 +99,32 @@ private[preview] class SiteTransformer[F[_]: Async]( } } + private def transformASTLazily(tree: ParsedTree[F], html: ResultMap[F]): F[ResultMap[F]] = { + + val transformer = for { + modifiedRoot <- Async[F].delay( + tree.modifyRoot(ASTPageTransformer.transform(_, parser.config)) + ) + resultMap <- transformHTML(modifiedRoot, astRenderer) + } yield resultMap + + def buildLazyMap(delegate: F[ResultMap[F]]): ResultMap[F] = { + html.keySet + .map(ASTPageTransformer.ASTPathTranslator.translateASTPath) + .map { astPath => + val result = LazyResult(delegate.map(_.get(astPath))) + (astPath, result: SiteResult[F]) + } + .toMap + } + + transformer.memoize.map(buildLazyMap) + } + val transform: F[SiteResults[F]] = for { tree <- parse html <- transformHTML(tree, htmlRenderer) - ast <- transformHTML( - tree.modifyRoot(ASTPageTransformer.transform(_, parser.config)), - astRenderer - ) + ast <- transformASTLazily(tree, html) ebooks <- Async[F].fromEither(transformBinaries(tree).leftMap(ConfigException.apply)) } yield { new SiteResults(staticFiles ++ ast ++ html ++ ebooks) @@ -201,3 +221,6 @@ private[preview] case class RenderedResult[F[_]: Async](content: String) extends private[preview] case class StaticResult[F[_]: Async](content: fs2.Stream[F, Byte]) extends SiteResult[F] + +private[preview] case class LazyResult[F[_]: Async](result: F[Option[SiteResult[F]]]) + extends SiteResult[F]