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

dotty.tools.scaladoc.site.StaticSiteLoader.scala Maven / Gradle / Ivy

There is a newer version: 3.6.0-RC1-bin-20240903-21a3d39-NIGHTLY
Show newest version
package dotty.tools.scaladoc
package site

import java.io.File
import java.nio.file.Files
import java.nio.file.{ Paths, Path }
import scala.io._
import dotty.tools.scaladoc.site.BlogParser

class StaticSiteLoader(val root: File, val args: Scaladoc.Args)(using StaticSiteContext, CompilerContext):
  val ctx: StaticSiteContext = summon[StaticSiteContext]

  val possibleYamlFiles: Seq[String] = Seq(
    "sidebar.yml"
  )

  def load(): StaticSiteRoot = {
    // Check whether there's YAML file defining static site structure
    possibleYamlFiles
      .map(name => root.toPath.resolve(name))
      .find(path => Files.exists(path))
      .fold(loadBasedOnFileSystem()) { path =>
        val entries = Sidebar.load(path.toFile)
        loadBasedOnYaml(entries)
      }
  }

  /** Method loading static site structure based on YAML configuration file.
   *
   * The rendered static site will only contain pages that are present in YAML.
   * The following rules are applied:
   *  - Each subsection will be a separate directory.
   *  - Nested subsections will result in nested directories.
   *  - If the subsection object contains location of index and doesn't contain any item,
   *    items are loaded using file system from the directory of the index file.
   *  - By default, directory name is a subsection title converted to kebab case.
   *    However, you can override default name by setting "directory" property of the subsection object.
   *
   */
  def loadBasedOnYaml(yamlRoot: Sidebar.Category): StaticSiteRoot = {
    val rootDest = ctx.docsPath.resolve("index.html").toFile
    val rootIndex = yamlRoot.indexPath
      .map(ctx.docsPath.resolve(_).toFile)
      .filter(_.exists)
      .fold(emptyTemplate(rootDest, "index")) { f =>
        val loaded = loadTemplateFile(f)
        if loaded.title.name != "index"
        then report.warn("Property `title` will be overridden by project name", f)
        loaded
      }.copy(title = TemplateName.FilenameDefined(args.name))

    def loadChild(pathFromRoot: Path): Sidebar => LoadedTemplate = {
      case Sidebar.Category(optionTitle, optionIndexPath, nested, dir) =>
        val indexPageOpt = optionIndexPath
          .map(relativizeIfNeeded)
          .map(_.toFile)
          .filter(_.exists)
          .map(loadTemplateFile(_))
        val title = (
          optionTitle.map(TemplateName.SidebarDefined(_)) ++
          indexPageOpt.map(_.title)
        ).headOption.getOrElse {
          report.error(s"Title for subsection needs to be set in YAML config or in index file")
          TemplateName.FilenameDefined("unnamed_section")
        }
        val categoryPath = dir.fold(pathFromRoot.resolve(toKebabCase(title.name)))(pathFromRoot.resolve(_))
        val indexPage = indexPageOpt.getOrElse(emptyTemplate(categoryPath.resolve("index.html").toFile, title.name))

        val children = optionIndexPath.filter(_ => nested.isEmpty).fold(
          nested.map(child => loadChild(categoryPath)(child))
        ) { indexPath =>
          val indexPathDirectory = Paths.get(indexPath).getParent
          val filesInDirectory = Option(root.toPath.resolve(indexPathDirectory).toFile.listFiles)
          filesInDirectory.fold(List.empty) { files =>
            val mappingFunc: File => File = file => {
              val relativeFile = root.toPath.resolve(indexPathDirectory).relativize(file.toPath)
              categoryPath.resolve(relativeFile).toFile
            }
            files.toList
              .filter(_.toPath != indexPage.file.toPath)
              .flatMap(file => loadRecursively(file, mappingFunc))
          }
        }

        LoadedTemplate(indexPage, children, categoryPath.resolve("index.html").toFile)
      case Sidebar.Page(optionTitle, pagePath, hidden) =>
        val path = relativizeIfNeeded(pagePath)
        val file = path.toFile
        val title = optionTitle.map(TemplateName.SidebarDefined(_))
        val templateFile = loadTemplateFile(file, title)
        LoadedTemplate(templateFile, List.empty, pathFromRoot.resolve(file.getName).toFile, hidden)
    }
    val rootTemplate = LoadedTemplate(rootIndex, yamlRoot.nested.map(c => loadChild(ctx.docsPath)(c)) ++ loadBlog(), rootDest)
    val mappings = createMapping(rootTemplate)
    StaticSiteRoot(rootTemplate, mappings)
  }

  def loadBasedOnFileSystem(): StaticSiteRoot = {
    val rootTemplate =
      loadRecursively(ctx.docsPath.toFile).getOrElse(
        LoadedTemplate(emptyTemplate(ctx.docsPath.resolve("index.html").toFile, "index"), List.empty, ctx.docsPath.resolve("index.html").toFile)
      )

    if rootTemplate.templateFile.title.name != "index" then {
      report.warn("Property `title` will be overridden by project name", rootTemplate.templateFile.file)
    }

    val withChangedTitle =
      rootTemplate.copy(templateFile = rootTemplate.templateFile.copy(title = TemplateName.FilenameDefined(args.name)))

    val withBlog = loadBlog().fold(withChangedTitle)(blog => withChangedTitle.copy(children = withChangedTitle.children :+ blog))

    val mappings = createMapping(withBlog)
    StaticSiteRoot(withBlog, mappings)
  }

  def loadBlog(): Option[LoadedTemplate] = {
    val blogConfig = BlogParser.readYml(root)
    val rootPath = Option(blogConfig.input).map(input => ctx.resolveNewBlogPath(input)).getOrElse(ctx.blogPath)
    val defaultDirectory = Option(blogConfig.output).getOrElse("blog")

    type Date = (String, String, String)
    if (!Files.exists(rootPath) || blogConfig.hidden) None
    else {
      val indexPageOpt = Seq(
          rootPath.resolve("index.md"),
          rootPath.resolve("index.html"),
      ).filter(p => Files.exists(p)) match {
          case Nil => None
          case indexPath :: Nil => Some(indexPath)
          case list =>
            report.warning(s"Multiple index pages for $rootPath found in ${list.map(_.toFile)}. Defaulting to first.")
            list.headOption
      }
      val indexTemplateOpt = indexPageOpt.map(p => loadTemplateFile(p.toFile))

      val indexPage = indexTemplateOpt.getOrElse(emptyTemplate(rootPath.resolve("index.html").toFile, "Blog"))
      val indexDest = ctx.docsPath.resolve(defaultDirectory).resolve("index.html")
      val regex = raw"(\d*)-(\d*)-(\d*)-(.*)".r
      def splitDateName(tf: TemplateFile): (Date, String) = tf.file.getName match
          case regex(year, month, day, name) => ((year, month, day), name)
          case name =>
            report.warn("Incorrect file name for blog post. Post file name should be in format ---", tf.file)
            (("1900","01","01"), name)

      def dateFrom(tf: TemplateFile, default: String = "1900-01-01"): String =
        val pageSettings = tf.settings.get("page").collect{ case m: Map[String @unchecked, ?] => m }
        pageSettings.flatMap(_.get("date").collect{ case s: String => s}).getOrElse(default) // blogs without date are last

      val posts = List(rootPath.resolve("_posts"))
        .filter(Files.exists(_))
        .flatMap(_.toFile.listFiles)
        .filterNot(_.isDirectory)
        .map { postFile =>
          val templateFile = loadTemplateFile(postFile)
          val ((year, month, day), name) = splitDateName(templateFile)
          val destPath = ctx.docsPath.resolve(defaultDirectory).resolve(year).resolve(month).resolve(day).resolve(name)
          val date = dateFrom(templateFile, s"$year-$month-$day")
          date -> LoadedTemplate(templateFile, List.empty, destPath.toFile)
        }.sortBy(_._1).reverse.map(_._2)

      Some(LoadedTemplate(indexPage, posts, indexDest.toFile))
    }
  }

  def loadRecursively(currRoot: File, destMappingFunc: File => File = identity): Option[LoadedTemplate] = {
    val rootPath = currRoot.toPath
    if currRoot.isDirectory
    then {
      val indexPageOpt = Seq(
        rootPath.resolve("index.md"),
        rootPath.resolve("index.html"),
        rootPath.resolve(s"${currRoot.getName}.md"),
        rootPath.resolve(s"${currRoot.getName}.html")
      ).filter(p => Files.exists(p)) match {
        case Nil => None
        case indexPath :: Nil => Some(indexPath)
        case list =>
          report.warning(s"Multiple index pages for $currRoot found in ${list.map(_.toFile)}. Defaulting to first.")
          list.headOption
      }
      val templateFileOpt = indexPageOpt.map(p => loadTemplateFile(p.toFile))

      val indexPage = templateFileOpt.getOrElse(emptyTemplate(rootPath.resolve("index.html").toFile, "index"))

      val children = currRoot.listFiles.toList
        .filter(_.toPath != indexPageOpt.getOrElse(null))
      Some(LoadedTemplate(indexPage, children.flatMap(loadRecursively(_, destMappingFunc)).sortBy(_.templateFile.title.name), destMappingFunc(indexPage.file)))
    }
    else if (currRoot.exists && ctx.siteExtensions.exists(ext => currRoot.getName.endsWith(ext))) {
      val templateFile = loadTemplateFile(currRoot)
      Some(LoadedTemplate(templateFile, List.empty, destMappingFunc(templateFile.file)))
    }
    else None
  }

  def createMapping(root: LoadedTemplate): Map[Path, Path] =
    Map((root.templateFile.file.toPath, root.file.toPath)) ++ root.children.map(createMapping).fold(Map.empty)(_ ++ _)

  private def toKebabCase(s: String) = s.toLowerCase.replace(' ', '-')

  private def relativizeIfNeeded(link: String): Path =
    val path = Paths.get(link)
    if !path.isAbsolute then ctx.docsPath.resolve(link)
    else path

  extension (p: Path)
    private def toHtml: Path = p.getParent.resolve(p.getFileName.toString match
      case f if f.endsWith(".md") => f.stripSuffix(".md") + ".html"
      case f => f
    )




© 2015 - 2024 Weber Informatics LLC | Privacy Policy