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

laika.io.model.InputTreeBuilder.scala Maven / Gradle / Ivy

/*
 * Copyright 2012-2022 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package laika.io.model

import cats.data.Kleisli
import cats.effect.Async
import fs2.io.file.Files
import laika.ast.Path.Root
import laika.ast.{Document, DocumentType, Path, StaticDocument, StyleDeclaration, StyleDeclarationSet, TemplateDocument, TextDocumentType}
import laika.bundle.{DocumentTypeMatcher, Precedence}
import laika.config.Config
import laika.io.descriptor.TreeInputDescriptor
import laika.io.model.InputTree.{BuilderContext, BuilderStep}
import laika.io.runtime.DirectoryScanner
import laika.io.runtime.ParserRuntime.{MissingDirectory, ParserErrors}
import laika.io.runtime.TreeResultBuilder.{ConfigResult, DocumentResult, ParserResult, StyleResult, TemplateResult}

import java.io.{File, InputStream}
import scala.io.Codec
import scala.reflect.ClassTag

/** Builder API for freely constructing input trees from directories, files, classpath resources, in-memory strings
  * or pre-constructed AST trees.
  *
  * If your input is just one or more directories, you can use the corresponding shortcuts on the parser or
  * transformer instances, e.g. `transformer.fromDirectory(...).toDirectory(...)`.
  * This builder is meant to be used for situations where more flexibility is required.
  *
  * All the specified inputs will be combined into a single logical tree and each document gets a virtual path
  * assigned that describes its logical position within the tree.
  * As a consequence all cross-linking or referencing of images can happen within the virtual path abstraction,
  * meaning a resource from the file system can point to an input constructed in-memory via a relative, virtual path.
  *
  * When adding strings, files or directories you need to specify a "mount point" that signals where within
  * the virtual tree the inputs should be placed.
  * For adding directories the mount point is optional, when omitted the directory becomes the virtual root of
  * the input tree.
  *
  * The resulting input tree can be passed to parsers, transformers and theme builders.
  *
  * Example for composing inputs from two directories, a template loaded from the classpath and a CSS
  * file generated in-memory:
  *
  * {{{
  * val inputs = InputTree[F]
  *   .addDirectory("/path-to-my/markup-files")
  *   .addDirectory("/path-to-my/images", Root / "images")
  *   .addClasspathResource("my-templates/default.template.html", DefaultTemplatePath.forHTML)
  *   .addString(generateMyStyles(), Root / "css" / "site.css")
  * }}}
  *
  * These inputs can then be configured for the sbt plugin:
  *
  * {{{
  *   laikaInputs := inputs
  * }}}
  *
  * Or passed to a `TreeTransformer` instance:
  *
  * {{{
  * val res: F[RenderedTreeRoot[F]] = transformer.use {
  *   _.fromInputTree(inputs)
  *    .toDirectory("target")
  *    .transform
  * }
  * }}}
  */
class InputTreeBuilder[F[_]](private[laika] val exclude: FileFilter,
                             private[model] val steps: Vector[BuilderStep[F]],
                             private[laika] val fileRoots: Vector[FilePath])(implicit F: Async[F]) {

  import cats.implicits._

  private def addStep (step: BuilderStep[F]): InputTreeBuilder[F] =
    addStep(None)(step)

  private def addStep (newFileRoot: Option[FilePath])(step: BuilderStep[F]): InputTreeBuilder[F] =
    new InputTreeBuilder(exclude, steps = steps :+ step, newFileRoot.fold(fileRoots)(fileRoots :+ _))

  private def addStep (path: Path, newFileRoot: Option[FilePath] = None)
                      (f: PartialFunction[DocumentType, InputTree[F] => InputTree[F]]): InputTreeBuilder[F] =
    addStep(newFileRoot) {
      Kleisli { ctx =>
        val m = f.applyOrElse[DocumentType, InputTree[F] => InputTree[F]](ctx.docTypeMatcher(path), _ => identity)
        ctx.modifyTree(m).pure[F]
      }
    }

  private def addParserResult (result: ParserResult): InputTreeBuilder[F] = addStep {
    Kleisli[F, BuilderContext[F], BuilderContext[F]](_.modifyTree(_ + result).pure[F])
  }

  /** Adds the specified directories to the input tree, merging them all into a single virtual root, recursively.
    */
  def addDirectories (dirs: Seq[FilePath])(implicit codec: Codec): InputTreeBuilder[F] = dirs.foldLeft(this) {
    case (builder, dir) => builder.addDirectory(dir)
  }

  /** Adds the specified directories to the input tree, placing it in the virtual root.
    */
  def addDirectory (name: String)(implicit codec: Codec): InputTreeBuilder[F] =
    addDirectory(FilePath.parse(name), Root)

  /** Adds the specified directories to the input tree, placing it at the specified mount point in the virtual tree.
    */
  def addDirectory (name: String, mountPoint: Path)(implicit codec: Codec): InputTreeBuilder[F] =
    addDirectory(FilePath.parse(name), mountPoint)

  @deprecated("use addDirectory(String) or addDirectory(FilePath)", "0.19.0")
  def addDirectory (dir: File)(implicit codec: Codec): InputTreeBuilder[F] =
    addDirectory(FilePath.fromJavaFile(dir), Root)

  /** Adds the specified directories to the input tree, placing it in the virtual root.
    */
  def addDirectory (dir: FilePath)(implicit codec: Codec): InputTreeBuilder[F] = addDirectory(dir, Root)

  @deprecated("use addDirectory(String, Path) or addDirectory(FilePath, Path)", "0.19.0")
  def addDirectory (dir: File, mountPoint: Path)(implicit codec: Codec): InputTreeBuilder[F] =
    addDirectory(FilePath.fromJavaFile(dir), mountPoint)

  /** Adds the specified directories to the input tree, placing it at the specified mount point in the virtual tree.
    */
  def addDirectory (dir: FilePath, mountPoint: Path)(implicit codec: Codec): InputTreeBuilder[F] = addStep(Some(dir)) {
    Kleisli { ctx =>
      Files[F].isDirectory(dir.toFS2Path).ifM(
        DirectoryScanner
          .scanDirectories[F](new DirectoryInput(Seq(dir), codec, ctx.docTypeMatcher, ctx.exclude, mountPoint))
          .map(res => ctx.modifyTree(_ ++ res)),
        ctx.withMissingDirectory(dir).pure[F]
      )
    }
  }

  /** Adds the specified file to the input tree, placing it at the specified mount point in the virtual tree.
    *
    * The content type of the stream will be determined by the suffix of the virtual path, e.g.
    * `doc.md` would be passed to the markup parser, `doc.template.html` to the template parser, and so on.
    */
  def addFile (name: String, mountPoint: Path)(implicit codec: Codec): InputTreeBuilder[F] =
    addFile(FilePath.parse(name), mountPoint)

  @deprecated("use addFile(String) or addFile(FilePath)", "0.19.0")
  def addFile (file: File, mountPoint: Path)(implicit codec: Codec): InputTreeBuilder[F] =
    addFile(FilePath.fromJavaFile(file), mountPoint)

  /** Adds the specified file to the input tree, placing it at the specified mount point in the virtual tree.
    *
    * The content type of the stream will be determined by the suffix of the virtual path, e.g.
    * `doc.md` would be passed to the markup parser, `doc.template.html` to the template parser, and so on.
    */
  def addFile (file: FilePath, mountPoint: Path)(implicit codec: Codec): InputTreeBuilder[F] =
    addStep(mountPoint, Some(file)) {
      case DocumentType.Static(formats) => _ + BinaryInput.fromFile(file, mountPoint, formats)
      case docType: TextDocumentType    => _ + TextInput.fromFile(file, mountPoint, docType)
    }

  /** Adds the specified classpath resource to the input tree, placing it at the specified mount point in the virtual tree.
    * The specified name must be compatible with Java's `Class.getResource`.
    * Relative paths will be interpreted as relative to the package name of the referenced class,
    * with all `.` replaced by `/`.
    *
    * The content type of the stream will be determined by the suffix of the virtual path, e.g.
    * `doc.md` would be passed to the markup parser, `doc.template.html` to the template parser, and so on.
    */
  def addClassResource[T: ClassTag] (name: String,
                                     mountPoint: Path)
                                    (implicit codec: Codec): InputTreeBuilder[F] =
    addStep(mountPoint) {
      case DocumentType.Static(formats) =>
        _ + BinaryInput.fromClassResource[F,T](name, mountPoint, formats)
      case docType: TextDocumentType =>
        _ + TextInput.fromClassResource[F,T](name, mountPoint, docType)
    }
  
  /** Adds the specified classpath resource to the input tree, placing it at the specified mount point in the virtual tree.
    * The specified name must be compatible with Java's `ClassLoader.getResource`.
    * The optional `ClassLoader` argument can be used to ensure the resource is found in an application or plugin
    * that uses multiple class loaders. 
    * If the call site is in the same module as the classpath resource, simply using `getClass.getClassLoader` should suffice. 
    *
    * The content type of the stream will be determined by the suffix of the virtual path, e.g.
    * `doc.md` would be passed to the markup parser, `doc.template.html` to the template parser, and so on.
    */
  def addClassLoaderResource (name: String,
                              mountPoint: Path,
                              classLoader: ClassLoader = getClass.getClassLoader)
                             (implicit codec: Codec): InputTreeBuilder[F] =
    addStep(mountPoint) {
      case DocumentType.Static(formats) =>
        _ + BinaryInput.fromClassLoaderResource(name, mountPoint, formats, classLoader)
      case docType: TextDocumentType =>
        _ + TextInput.fromClassLoaderResource(name, mountPoint, docType, classLoader)
    }

  @deprecated("Use addClassResource or addClassLoaderResource", "0.19.0")
  def addClasspathResource (name: String, mountPoint: Path)(implicit codec: Codec): InputTreeBuilder[F] =
    addClassLoaderResource(name, mountPoint)

  @deprecated("use addInputStream", "0.19.0")
  def addStream (stream: F[InputStream],
                 mountPoint: Path,
                 autoClose: Boolean = true)
                (implicit codec: Codec): InputTreeBuilder[F] = addInputStream(stream, mountPoint, autoClose)

  /** Adds the specified input stream to the input tree, placing it at the specified mount point in the virtual tree.
    *
    * The content type of the stream will be determined by the suffix of the virtual path, e.g.
    * `doc.md` would be passed to the markup parser, `doc.template.html` to the template parser, and so on.
    *
    * The `autoClose` argument indicates whether the stream should be closed after use.
    * In some integration scenarios with 3rd-party libraries, e.g. for PDF creation, `autoClose` is not
    * guaranteed as the handling of the stream is entirely managed by the 3rd party tool.
    */
  def addInputStream (stream: F[InputStream],
                      mountPoint: Path,
                      autoClose: Boolean = true)
                     (implicit codec: Codec): InputTreeBuilder[F] =
    addStep(mountPoint) {
      case DocumentType.Static(formats) =>
        _ + BinaryInput.fromInputStream(stream, mountPoint,autoClose, formats)
      case docType: TextDocumentType =>
        _ + TextInput.fromInputStream(stream, mountPoint, docType, autoClose)
    }

  /** Adds the specified input stream to the input tree, placing it at the specified mount point in the virtual tree.
    *
    * The content type of the stream will be determined by the suffix of the virtual path, e.g.
    * `doc.md` would be passed to the markup parser, `doc.template.html` to the template parser, and so on.
    *
    * If the content type is text-based the stream will be decoded as UTF-8.
    * In case a different codec is required, use `addTextStream` and decode the text beforehand.
    */
  def addBinaryStream (stream: fs2.Stream[F, Byte],
                       mountPoint: Path): InputTreeBuilder[F] =
    addStep(mountPoint) {
      case DocumentType.Static(formats) =>
        _ + BinaryInput(stream, mountPoint, formats)
      case docType: TextDocumentType =>
        _ + TextInput(stream.through(fs2.text.utf8.decode).compile.string, mountPoint, docType)
    }

  /** Adds the specified input stream to the input tree, placing it at the specified mount point in the virtual tree.
    *
    * The content type of the stream will be determined by the suffix of the virtual path, e.g.
    * `doc.md` would be passed to the markup parser, `doc.template.html` to the template parser, and so on.
    *
    * If the target content type is binary the stream will be encoded as UTF-8.
    * In case a different codec is required, use `addBinaryStream` and encode the text beforehand.
    */
  def addTextStream (stream: fs2.Stream[F, String],
                     mountPoint: Path): InputTreeBuilder[F] =
    addStep(mountPoint) {
      case DocumentType.Static(formats) =>
        _ + BinaryInput(stream.through(fs2.text.utf8.encode), mountPoint, formats)
      case docType: TextDocumentType =>
        _ + TextInput(stream.compile.string, mountPoint, docType)
    }

  /** Adds the specified string resource to the input tree, placing it at the specified mount point in the virtual tree.
    *
    * The content type of the stream will be determined by the suffix of the virtual path, e.g.
    * * `doc.md` would be passed to the markup parser, `doc.template.html` to the template parser, and so on.
    */
  def addString (input: String, mountPoint: Path): InputTreeBuilder[F] =
    addStep(mountPoint) {
      case DocumentType.Static(formats) => _ + BinaryInput.fromString(input, mountPoint,formats)
      case docType: TextDocumentType    => _ + TextInput.fromString[F](input, mountPoint, docType)
    }

  /** Adds the specified document AST to the input tree, by-passing the parsing step.
    *
    * In some cases when generating input on the fly, it might be more convenient or more type-safe to construct
    * the AST directly than to generate the text markup as input for the parser.
    */
  def addDocument (doc: Document): InputTreeBuilder[F] = addParserResult(DocumentResult(doc))

  /** Adds the specified template AST to the input tree, by-passing the parsing step.
    *
    * In some cases when generating input on the fly, it might be more convenient or more type-safe to construct
    * the AST directly than to generate the template as a string as input for the template parser.
    */
  def addTemplate (doc: TemplateDocument): InputTreeBuilder[F] = addParserResult(TemplateResult(doc))

  /** Adds the specified configuration instance and assigns it to the specified tree path in a way
    * that is equivalent to having a HOCON file called `directory.conf` in that directory.
    */
  def addConfig (config: Config, treePath: Path): InputTreeBuilder[F] = addParserResult(ConfigResult(treePath, config))

  /** Adds the specified styles for PDF to the input tree.
    * These type of style declarations are only used in the context of Laika's "CSS for PDF" support 
    * which works slightly differently than web CSS as PDF generation in Laika is not based on interim HTML results.
    */
  def addStyles (styles: Set[StyleDeclaration], path: Path, precedence: Precedence = Precedence.High): InputTreeBuilder[F] =
    addParserResult(StyleResult(StyleDeclarationSet(Set(path), styles, precedence), "fo"))

  /** Adds a path to the input tree that represents a document getting processed by some external tool.
    * Such a path will be used in link validation, but no further processing for this document will be performed. 
    */
  def addProvidedPath (path: Path): InputTreeBuilder[F] = addStep(path) {
    case DocumentType.Static(formats) => _ + StaticDocument(path, formats)
    case _                            => _ + StaticDocument(path)
  }

  /** Adds the specified paths to the input tree that represent documents getting processed by some external tool.
    * Such a path will be used in link validation, but no further processing for this document will be performed. 
    */
  def addProvidedPaths (paths: Seq[Path]): InputTreeBuilder[F] = paths.foldLeft(this) {
    case (builder, path) => builder.addProvidedPath(path)
  }

  /** Adds the specified file filter to this input tree.
    *
    * The filter will only be used for scanning directories when calling `addDirectory` on this builder,
    * not for any of the other methods.
    */
  def withFileFilter (newFilter: FileFilter): InputTreeBuilder[F] = {
    new InputTreeBuilder(exclude.orElse(newFilter), steps, fileRoots)
  }

  /** Merges this input tree with the specified tree, recursively.
    */
  def merge (other: InputTreeBuilder[F]): InputTreeBuilder[F] =
    new InputTreeBuilder(exclude.orElse(other.exclude), steps ++ other.steps, fileRoots ++ other.fileRoots)

  /** Merges this input tree with the specified tree, recursively.
    */
  def merge (other: InputTree[F]): InputTreeBuilder[F] = addStep {
    Kleisli { (ctx: BuilderContext[F]) =>
      ctx.modifyTree(_ ++ other).pure[F]
    }
  }


  /** Builds the tree based on the inputs added to this instance.
    *
    * The method is effectful as it might involve scanning directories to determine the tree structure.
    *
    * This method is normally not called by application code directly, as the parser and transformer APIs
    * expect an `InputTreeBuilder` instance.
    */
  def build: F[InputTree[F]] = build(DocumentTypeMatcher.base)

  /** Builds the tree based on the inputs added to this instance and the specified custom document type matcher.
    *
    * The method is effectful as it might involve scanning directories to determine the tree structure.
    *
    * This method is normally not called by application code directly, as the parser and transformer APIs
    * expect an `InputTreeBuilder` instance.
    */
  def build (docTypeMatcher: Path => DocumentType): F[InputTree[F]] = {
    val ctx = BuilderContext(exclude, docTypeMatcher, InputTree.empty[F])
    build(ctx).flatMap { res =>
      res.missingDirectories match {
        case Nil => res.input.copy(sourcePaths = fileRoots).pure[F]
        case missing => F.raiseError(ParserErrors(missing.map(MissingDirectory(_)).toSet))
      }
    }
  }

  /** Provides a description of this input tree.
    * Input directories will be scanned or reported in case they don't exist.
    * Documents added to this builder individually will be listed without additional checks.
    *
    * This functionality is mostly intended for tooling support.
    */
  def describe (docTypeMatcher: Path => DocumentType): F[TreeInputDescriptor] = {
    val ctx = BuilderContext(exclude, docTypeMatcher, InputTree.empty[F])
    build(ctx).map { res =>
      val validSourcePaths = fileRoots.diff(res.missingDirectories)
      TreeInputDescriptor.create(res.input.copy(sourcePaths = validSourcePaths), res.missingDirectories)
    }
  }

  private def build (ctx: BuilderContext[F]): F[BuilderContext[F]] =
    steps
      .reduceLeftOption(_ andThen _)
      .fold(ctx.pure[F])(_.run(ctx))

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy