diff --git a/.gitignore b/.gitignore index af515ad..ee44a96 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ .idea -target/ +target diff --git a/src/main/scala/com/typesafe/sbt/SbtSite.scala b/src/main/scala/com/typesafe/sbt/SbtSite.scala index c22bbf3..30ca61e 100644 --- a/src/main/scala/com/typesafe/sbt/SbtSite.scala +++ b/src/main/scala/com/typesafe/sbt/SbtSite.scala @@ -1,88 +1,59 @@ package com.typesafe.sbt -import com.typesafe.sbt.site.AsciidoctorSupport import sbt._ import Keys._ import com.typesafe.sbt.site._ - -object SbtSite extends Plugin { - object SiteKeys { +object SbtSite extends AutoPlugin { + override def trigger = allRequirements + object autoImport { val makeSite = TaskKey[File]("make-site", "Generates a static website for a project.") val packageSite = TaskKey[File]("package-site", "Create a zip file of the website.") - - // Helper to point at mappings for the site. - val siteMappings = mappings in makeSite - val siteDirectory = target in makeSite - val siteSources = sources in makeSite - val siteSourceDirectory = sourceDirectory in makeSite - - val previewSite = TaskKey[Unit]("preview-site", "Launches a jetty server that serves your generated site from the target directory") - val previewFixedPort = SettingKey[Option[Int]]("previewFixedPort") in previewSite - val previewLaunchBrowser = SettingKey[Boolean]("previewLaunchBrowser") in previewSite } + import autoImport._ + + // Helper to point at mappings for the site. + private[sbt] val siteMappings = mappings in makeSite + private[sbt] val siteDirectory = target in makeSite + private[sbt] val siteSources = sources in makeSite + private[sbt] val siteSourceDirectory = sourceDirectory in makeSite + + override lazy val projectSettings = Seq( + siteMappings := Seq.empty, + siteMappings <<= siteMappings ?? Seq.empty, + siteDirectory := target.value / "site", + siteSourceDirectory := sourceDirectory.value / "site", + includeFilter in makeSite := "*.html" | "*.css" | "*.png" | "*.jpg" | "*.gif" | "*.js" | "*.swf", + siteMappings ++= SiteHelpers.selectSubpaths(siteSourceDirectory.value, (includeFilter in makeSite).value), + makeSite := SiteHelpers.copySite(siteDirectory.value, streams.value.cacheDirectory, siteMappings.value), + artifact in packageSite := SiteHelpers.siteArtifact(moduleName.value), + artifactPath in packageSite <<= Defaults.artifactPathSetting(artifact in packageSite), + packageSite := SiteHelpers.createSiteZip(makeSite.value, (artifactPath in packageSite).value, streams.value) + ) object site { - import SiteKeys._ - val settings = Seq( - siteMappings := Seq.empty, - siteDirectory := target.value / "site", - siteSourceDirectory := sourceDirectory.value / "site", - includeFilter in makeSite := "*.html" | "*.css" | "*.png" | "*.jpg" | "*.gif" | "*.js" | "*.swf", - siteMappings ++= selectSubpaths(siteSourceDirectory.value, (includeFilter in makeSite).value), - makeSite := copySite(siteDirectory.value, streams.value.cacheDirectory, siteMappings.value), - artifact in packageSite := siteArtifact(moduleName.value), - artifactPath in packageSite <<= Defaults.artifactPathSetting(artifact in packageSite), - packageSite := createSiteZip(makeSite.value, (artifactPath in packageSite).value, streams.value) - ) ++ Preview.settings - - /** Convenience functions to add a task of mappings to a site under a nested directory. */ - def addMappingsToSiteDir(mappings: TaskKey[Seq[(File,String)]], nestedDirectory: String): Setting[_] = - siteMappings <++= mappings map { m => - for((f, d) <- m) yield (f, nestedDirectory + "/" + d) - } - - /** Includes scaladoc APIS in site under a "latest/api" directory. */ - def includeScaladoc(alias: String = "latest/api"): Seq[Setting[_]] = - Seq(addMappingsToSiteDir(mappings in packageDoc in Compile, alias)) - /** Includes Jekyll generated site under the root directory. */ - def jekyllSupport(alias: String = ""): Seq[Setting[_]] = - JekyllSupport.settings() ++ Seq(addMappingsToSiteDir(mappings in JekyllSupport.Jekyll, alias)) - /** Includes Sphinx generated site under the root directory. */ - def sphinxSupport(alias: String = ""): Seq[Setting[_]] = - SphinxSupport.settings() ++ Seq(addMappingsToSiteDir(mappings in SphinxSupport.Sphinx, alias)) - /** Includes Pamflet generate site under the root directory. */ - def pamfletSupport(alias: String = ""): Seq[Setting[_]] = - PamfletSupport.settings() ++ Seq(addMappingsToSiteDir(mappings in PamfletSupport.Pamflet, alias)) - /** Includes Nanoc generated site under the root directory. */ - def nanocSupport(alias: String = ""): Seq[Setting[_]] = - NanocSupport.settings() ++ Seq(addMappingsToSiteDir(mappings in NanocSupport.Nanoc, alias)) - def asciidoctorSupport(alias: String = ""): Seq[Setting[_]] = - AsciidoctorSupport.settings ++ Seq(addMappingsToSiteDir(mappings in AsciidoctorSupport.Asciidoctor, alias)) - def preprocessSite(alias: String = ""): Seq[Setting[_]] = - PreprocessSupport.settings() ++ Seq(addMappingsToSiteDir(mappings in PreprocessSupport.Preprocess, alias)) +// def preprocessSite(alias: String = ""): Seq[Setting[_]] = +// PreprocessSupport.settings() ++ Seq(addMappingsToSiteDir(mappings in PreprocessSupport.Preprocess, alias)) +// +// /** Includes scaladoc APIS in site under a "latest/api" directory. */ +// def includeScaladoc(alias: String = "latest/api"): Seq[Setting[_]] = +// Seq(SiteHelpers.addMappingsToSiteDir(mappings in packageDoc in Compile, alias)) +// /** Includes Jekyll generated site under the root directory. */ +// def jekyllSupport(alias: String = ""): Seq[Setting[_]] = +// JekyllSupport.settings() ++ Seq(SiteHelpers.addMappingsToSiteDir(mappings in JekyllSupport.autoImports.Jekyll, alias)) +// /** Includes Sphinx generated site under the root directory. */ +// def sphinxSupport(alias: String = ""): Seq[Setting[_]] = +// SphinxSupport.settings() ++ Seq(SiteHelpers.addMappingsToSiteDir(mappings in SphinxSupport.Sphinx, alias)) +// /** Includes Pamflet generate site under the root directory. */ +// def pamfletSupport(alias: String = ""): Seq[Setting[_]] = +// PamfletSupport.settings() ++ Seq(SiteHelpers.addMappingsToSiteDir(mappings in PamfletSupport.Pamflet, alias)) +// /** Includes Nanoc generated site under the root directory. */ +// def nanocSupport(alias: String = ""): Seq[Setting[_]] = +// NanocSupport.settings() ++ Seq(SiteHelpers.addMappingsToSiteDir(mappings in NanocSupport.Nanoc, alias)) +// def asciidoctorSupport(alias: String = ""): Seq[Setting[_]] = +// AsciidoctorSupport.settings ++ Seq(SiteHelpers.addMappingsToSiteDir(mappings in AsciidoctorSupport.Asciidoctor, alias)) def publishSite(): SettingsDefinition = addArtifact(artifact in packageSite, packageSite) } - // Note: We include helpers so other plugins can 'plug in' to this one without requiring users to use/configure the site plugin. - override val settings = Seq( - SiteKeys.siteMappings <<= SiteKeys.siteMappings ?? Seq.empty - ) - - def selectSubpaths(dir: File, filter: FileFilter): Seq[(File, String)] = Path.selectSubpaths(dir, filter).toSeq - - def copySite(dir: File, cacheDir: File, maps: Seq[(File, String)]): File = { - val concrete = maps map { case (file, dest) => (file, dir / dest) } - Sync(cacheDir / "make-site")(concrete) - dir - } - - def siteArtifact(name: String) = Artifact(name, Artifact.DocType, "zip", "site") - - def createSiteZip(siteDir: File, zipPath: File, s: TaskStreams): File = { - IO.zip(Path.allSubpaths(siteDir), zipPath) - s.log.info("Site packaged: " + zipPath) - zipPath - } } diff --git a/src/main/scala/com/typesafe/sbt/site/AsciidoctorSupport.scala b/src/main/scala/com/typesafe/sbt/site/AsciidoctorSupport.scala index 43eed8a..203f2ac 100644 --- a/src/main/scala/com/typesafe/sbt/site/AsciidoctorSupport.scala +++ b/src/main/scala/com/typesafe/sbt/site/AsciidoctorSupport.scala @@ -2,26 +2,35 @@ package com.typesafe.sbt.site import java.util +import com.typesafe.sbt.SbtSite import org.asciidoctor.Asciidoctor.Factory import org.asciidoctor.{AsciiDocDirectoryWalker, Options, SafeMode} import sbt.Keys._ import sbt._ +object AsciidoctorSupport extends AutoPlugin { + override def requires = SbtSite + override def trigger = noTrigger -object AsciidoctorSupport { - - val Asciidoctor = config("asciidoctor") - - val settings: Seq[Setting[_]] = - Seq( - sourceDirectory in Asciidoctor <<= sourceDirectory(_ / "asciidoctor"), - target in Asciidoctor <<= target(_ / "asciidoctor"), - includeFilter in Asciidoctor := AllPassFilter) ++ inConfig(Asciidoctor)(Seq( - mappings <<= (sourceDirectory, target, includeFilter, version) map AsciidoctorRunner.run)) -} - -object AsciidoctorRunner { - - def run(input: File, output: File, includeFilter: FileFilter, version: String): Seq[(File, String)] = { + object autoImport { + val Asciidoctor = config("asciidoctor") + } + import autoImport._ + override def projectSettings: Seq[Setting[_]] = Seq( + sourceDirectory in Asciidoctor <<= sourceDirectory(_ / "asciidoctor"), + target in Asciidoctor <<= target(_ / "asciidoctor"), + includeFilter in Asciidoctor := AllPassFilter) ++ + inConfig(Asciidoctor)( + Seq( + mappings <<= (sourceDirectory, target, includeFilter, version) map run, + SiteHelpers.addMappingsToSiteDir(mappings, "TODO") + ) + ) + + private def run( + input: File, + output: File, + includeFilter: FileFilter, + version: String): Seq[(File, String)] = { val oldContextClassLoader = Thread.currentThread().getContextClassLoader Thread.currentThread().setContextClassLoader(this.getClass.getClassLoader) val asciidoctor = Factory.create() @@ -44,5 +53,4 @@ object AsciidoctorRunner { Thread.currentThread().setContextClassLoader(oldContextClassLoader) output ** includeFilter --- output pair relativeTo(output) } - } diff --git a/src/main/scala/com/typesafe/sbt/site/Generator.scala b/src/main/scala/com/typesafe/sbt/site/Generator.scala deleted file mode 100644 index e1202c5..0000000 --- a/src/main/scala/com/typesafe/sbt/site/Generator.scala +++ /dev/null @@ -1,40 +0,0 @@ -package com.typesafe.sbt -package site - -import sbt._ -import Keys._ - -object Generator { - def directorySettings(config: Configuration): Seq[Setting[_]] = - inConfig(config)(Seq( - sourceDirectory := sourceDirectory.value / config.name, - target := target.value / config.name - )) - def watchSettings(config: Configuration): Seq[Setting[_]] = - Seq( - watchSources in Global <++= (sourceDirectory in config) map { _.***.get } - ) - def checkGems(requirements: Map[String,String], s: TaskStreams): Unit = { - /** Generates an error message if the gem's version isn't appropriate for this build. */ - def makeError(gem: String, version: String, current: Option[String]): Option[String] = current match { - case Some(v) if v contains version => None - case Some(v) => Some("This build requires the gem [%s (%s)] but found version (%s) instead." format(gem, version, v)) - case None => Some("This build requires the gem [%s (%s)] to be installed." format (gem, version)) - } - val errors = for { - (gem, version) <- requirements - error <- makeError(gem, version, getGemVersion(gem)) - } yield error - if(errors.nonEmpty) - sys.error(errors.mkString("Gem version requirements failed:\n\t", "\n\t", "\n")) - } - - /** Checks versions of gems installed. */ - private def getGemVersion(gem: String): Option[String] = { - val installed = Seq("gem", "list", "--local", gem).!! - """\((.+)\)""".r.findFirstMatchIn(installed) match { - case None => None - case Some(m) => Some(m.group(1)) - } - } -} diff --git a/src/main/scala/com/typesafe/sbt/site/JekyllSupport.scala b/src/main/scala/com/typesafe/sbt/site/JekyllSupport.scala index 97fbb53..7120bf0 100644 --- a/src/main/scala/com/typesafe/sbt/site/JekyllSupport.scala +++ b/src/main/scala/com/typesafe/sbt/site/JekyllSupport.scala @@ -1,37 +1,42 @@ package com.typesafe.sbt package site +import sbt.Keys._ import sbt._ -import Keys._ -import SbtSite.SiteKeys.siteMappings +object JekyllSupport extends AutoPlugin { + override def requires = SbtSite + override def trigger = noTrigger -object JekyllSupport { - val Jekyll = config("jekyll") + object autoImport { + val Jekyll = config("jekyll") + val requiredGems = SettingKey[Map[String, String]]( + "jekyll-required-gems", "Required gem + versions for this build.") + val checkGems = TaskKey[Unit]( + "jekyll-check-gems", "Tests whether or not all required gems are available.") + } + import autoImport._ + override def projectSettings: Seq[Setting[_]] = + SiteHelpers.directorySettings(Jekyll) ++ + Seq( + includeFilter in Jekyll := ("*.html" | "*.png" | "*.js" | "*.css" | "*.gif" | "CNAME" | ".nojekyll"), + requiredGems := Map.empty + ) ++ inConfig(Jekyll)( + Seq( + checkGems := SiteHelpers.checkGems(requiredGems.value, streams.value), + mappings := { + val cg = checkGems.value + generate(sourceDirectory.value, target.value, includeFilter.value, streams.value) + }, + SiteHelpers.addMappingsToSiteDir(mappings, "TODO") + )) ++ SiteHelpers.watchSettings(Jekyll) - val requiredGems = SettingKey[Map[String,String]]("jekyll-required-gems", "Required gem + versions for this build.") - val checkGems = TaskKey[Unit]("jekyll-check-gems", "Tests whether or not all required gems are available.") - def settings(config: Configuration = Jekyll): Seq[Setting[_]] = - Generator.directorySettings(config) ++ - Seq( - includeFilter in config := ("*.html" | "*.png" | "*.js" | "*.css" | "*.gif" | "CNAME" | ".nojekyll"), - requiredGems := Map.empty - //(mappings in SiteKeys.siteMappings) <++= (mappings in Jekyll), - ) ++ inConfig(config)(Seq( - checkGems := Generator.checkGems(requiredGems.value, streams.value), - mappings := { - val cg = checkGems.value - JekyllImpl.generate(sourceDirectory.value, target.value, includeFilter.value, streams.value) - } - )) ++ Seq( - siteMappings ++= (mappings in config).value - ) ++ - Generator.watchSettings(config) // TODO - this may need to be optional. -} -/** Helper class with implementations of tasks. */ -object JekyllImpl { // TODO - Add command line args and the like. - final def generate(src: File, target: File, inc: FileFilter, s: TaskStreams): Seq[(File, String)] = { + final def generate( + src: File, + target: File, + inc: FileFilter, + s: TaskStreams): Seq[(File, String)] = { // Run Jekyll sbt.Process(Seq("jekyll", "build", "-d", target.getAbsolutePath), Some(src)) ! s.log match { case 0 => () @@ -39,7 +44,7 @@ object JekyllImpl { } // Figure out what was generated. for { - (file, name) <- (target ** inc --- target pair relativeTo(target)) + (file, name) <- target ** inc --- target pair relativeTo(target) } yield file -> name } } diff --git a/src/main/scala/com/typesafe/sbt/site/NanocSupport.scala b/src/main/scala/com/typesafe/sbt/site/NanocSupport.scala index 998c501..6761117 100644 --- a/src/main/scala/com/typesafe/sbt/site/NanocSupport.scala +++ b/src/main/scala/com/typesafe/sbt/site/NanocSupport.scala @@ -3,34 +3,44 @@ package site import sbt._ import Keys._ -import SbtSite.SiteKeys.siteMappings import collection.immutable import java.io.FileReader +/** Nanoc generator. */ +object NanocSupport extends AutoPlugin { + override def requires = SbtSite + override def trigger = noTrigger + object autoImport { + val Nanoc = config("nanoc") -object NanocSupport { - val Nanoc = config("nanoc") - - val requiredGems = SettingKey[Map[String,String]]("nanoc-required-gems", "Required gem + versions for this build.") - val checkGems = TaskKey[Unit]("nanoc-check-gems", "Tests whether or not all required gems are available.") - def settings(config: Configuration = Nanoc): Seq[Setting[_]] = - Generator.directorySettings(config) ++ - Seq( - includeFilter in config := AllPassFilter, - requiredGems := Map.empty - ) ++ inConfig(config)(Seq( - checkGems := Generator.checkGems(requiredGems.value, streams.value), - mappings := { - val cg = checkGems.value - NanocImpl.generate(sourceDirectory.value, target.value, includeFilter.value, streams.value) - } - )) ++ - Generator.watchSettings(config) // TODO - this may need to be optional. -} + val requiredGems = SettingKey[Map[String, String]]( + "nanoc-required-gems", "Required gem + versions for this build.") + val checkGems = TaskKey[Unit]( + "nanoc-check-gems", "Tests whether or not all required gems are available.") + } + import autoImport._ + override def projectSettings: Seq[Setting[_]] = + SiteHelpers.directorySettings(Nanoc) ++ + Seq( + includeFilter in Nanoc := AllPassFilter, + requiredGems := Map.empty + ) ++ inConfig(Nanoc)( + Seq( + checkGems := SiteHelpers.checkGems(requiredGems.value, streams.value), + mappings := { + val cg = checkGems.value + generate( + sourceDirectory.value, target.value, includeFilter.value, streams.value) + }, + SiteHelpers.addMappingsToSiteDir(mappings, "TODO") + )) ++ + SiteHelpers.watchSettings(Nanoc) -/** Helper class with implementations of tasks. */ -object NanocImpl { // TODO - Add command line args and the like. - final def generate(src: File, target: File, inc: FileFilter, s: TaskStreams): Seq[(File, String)] = { + private[sbt] def generate( + src: File, + target: File, + inc: FileFilter, + s: TaskStreams): Seq[(File, String)] = { // Run nanoc sbt.Process(Seq("nanoc"), Some(src)) ! s.log match { case 0 => () @@ -38,7 +48,8 @@ object NanocImpl { } val output = outputDir(src) if (output.getCanonicalPath != target.getCanonicalPath) { - s.log.warn(s"""Output directory ${output.toString} does not match the target ${target.toString}. + s.log.warn( + s"""Output directory ${output.toString} does not match the target ${target.toString}. We are going to copy the files over, but you might want to change ${yamlFileName(src)} so clean task cleans.""") IO.copyDirectory(output, target, overwrite = true, preserveLastModified = true) @@ -46,13 +57,13 @@ ${yamlFileName(src)} so clean task cleans.""") // Figure out what was generated. for { - (file, name) <- (target ** inc --- target pair relativeTo(target)) + (file, name) <- target ** inc --- target pair relativeTo(target) } yield file -> name } - def yamlFileName(src: File): File = src / "nanoc.yaml" + private[sbt] def yamlFileName(src: File): File = src / "nanoc.yaml" - def outputDir(src: File): File = { + private[sbt] def outputDir(src: File): File = { val yaml = nanocYaml(yamlFileName(src)) // it's output_dir in nanoc 3.x, and according to http://nanoc.ws/docs/nanoc-4-upgrade-guide/ it's // going to be changed to build_dir @@ -64,12 +75,12 @@ ${yamlFileName(src)} so clean task cleans.""") } } - def nanocYaml(configFile: File): immutable.Map[String, Any] = { + private[sbt] def nanocYaml(configFile: File): immutable.Map[String, Any] = { import org.yaml.snakeyaml.Yaml import java.util.{Map => JMap} import collection.JavaConversions._ if (!configFile.exists) { - sys.error(s"""$configFile is not found!""") + sys.error( s"""$configFile is not found!""") } val yaml = new Yaml() val x = yaml.load(new FileReader(configFile)).asInstanceOf[JMap[String, Any]] diff --git a/src/main/scala/com/typesafe/sbt/site/PamfletSupport.scala b/src/main/scala/com/typesafe/sbt/site/PamfletSupport.scala index a35ba50..ececdc2 100644 --- a/src/main/scala/com/typesafe/sbt/site/PamfletSupport.scala +++ b/src/main/scala/com/typesafe/sbt/site/PamfletSupport.scala @@ -4,24 +4,33 @@ package site import sbt._ import Keys._ -object PamfletSupport { - val Pamflet = config("pamflet") +/** Pamflet generator. */ +object PamfletSupport extends AutoPlugin { + import pamflet._ + override def requires = SbtSite + override def trigger = noTrigger + object autoImport { + val Pamflet = config("pamflet") + } - def settings(config: Configuration = Pamflet): Seq[Setting[_]] = - Generator.directorySettings(config) ++ - Seq( - // Note: txt is used for search index. - includeFilter in config := AllPassFilter - ) ++ inConfig(config)(Seq( - mappings <<= (sourceDirectory, target, includeFilter) map PamfletRunner.run - )) ++ - Generator.watchSettings(config) // TODO - this may need to be optional. -} + import autoImport._ -import pamflet._ + override def projectSettings: Seq[Setting[_]] = + SiteHelpers.directorySettings(Pamflet) ++ + Seq( + // Note: txt is used for search index. + includeFilter in Pamflet := AllPassFilter + ) ++ inConfig(Pamflet)( + Seq( + mappings <<= (sourceDirectory, target, includeFilter) map run, + SiteHelpers.addMappingsToSiteDir(mappings, "TODO") + )) ++ + SiteHelpers.watchSettings(Pamflet) -object PamfletRunner { - def run(input: File, output: File, includeFilter: FileFilter): Seq[(File, String)] = { + private[sbt] def run( + input: File, + output: File, + includeFilter: FileFilter): Seq[(File, String)] = { val storage = FileStorage(input) Produce(storage.globalized, output) output ** includeFilter --- output pair relativeTo(output) diff --git a/src/main/scala/com/typesafe/sbt/site/PreprocessSupport.scala b/src/main/scala/com/typesafe/sbt/site/PreprocessSupport.scala index 09b7d40..3a4164b 100644 --- a/src/main/scala/com/typesafe/sbt/site/PreprocessSupport.scala +++ b/src/main/scala/com/typesafe/sbt/site/PreprocessSupport.scala @@ -1,44 +1,62 @@ package com.typesafe.sbt.site -import java.io.PrintWriter - -import com.typesafe.sbt.SbtSite.SiteKeys._ +import com.typesafe.sbt.SbtSite +import com.typesafe.sbt.SbtSite._ import sbt.Keys._ import sbt._ import scala.util.matching.Regex +/** Provides ability to map values to `@` delimited variables. */ +object PreprocessSupport extends AutoPlugin { + override def requires = SbtSite + override def trigger = noTrigger + object autoImport { + val Preprocess = config("preprocess") -object PreprocessSupport { - val Preprocess = config("preprocess") + // Setting and task keys that can be used to set up preprocessing + val preprocessExts = SettingKey[Set[String]]( + "preprocess-exts", "Extensions of files to preprocess (not including dot).") + val preprocessVars = SettingKey[Map[String, String]]( + "preprocess-vars", "Replacements for preprocessing.") + val preprocess = TaskKey[File]("preprocess", "Preprocess a directory of files.") + } + import autoImport._ // Default variable replacement regex private[sbt] val Variable = """@(\w+)@""".r // Allows @@ -> @ replacement private[sbt] val defaultReplacements = Map("" -> "@") - // Setting and task keys that can be used to set up preprocessing - val preprocessExts = SettingKey[Set[String]]("preprocess-exts", "Extensions of files to preprocess (not including dot).") - val preprocessVars = SettingKey[Map[String, String]]("preprocess-vars", "Replacements for preprocessing.") - val preprocess = TaskKey[File]("preprocess", "Preprocess a directory of files.") - - def settings(config: Configuration = Preprocess): Seq[Setting[_]] = + override def projectSettings: Seq[Setting[_]] = Seq( + siteSourceDirectory in Preprocess := siteSourceDirectory.value, + siteDirectory in Preprocess := siteDirectory.value, + preprocessExts := Set("txt", "html", "md"), + preprocessVars := Map("VERSION" -> version.value), + includeFilter in Preprocess := AllPassFilter + ) ++ inConfig(Preprocess)( Seq( - siteSourceDirectory in Preprocess := siteSourceDirectory.value, - siteDirectory in Preprocess := siteDirectory.value, - preprocessExts := Set("txt", "html", "md"), - preprocessVars := Map("VERSION" -> version.value), - includeFilter in config := AllPassFilter - ) ++ inConfig(config)(Seq( sourceDirectory := sourceDirectory.value / "site-preprocess", - target := target.value / config.name, - preprocess := simplePreprocess(sourceDirectory.value, target.value, streams.value.cacheDirectory, preprocessExts.value, preprocessVars.value, streams.value.log), - mappings := gatherMappings(preprocess.value, includeFilter.value) - )) ++ Generator.watchSettings(config) + target := target.value / Preprocess.name, + preprocess := simplePreprocess( + sourceDirectory.value, target.value, streams.value.cacheDirectory, preprocessExts.value, + preprocessVars.value, streams.value.log), + mappings := gatherMappings(preprocess.value, includeFilter.value), + SiteHelpers.addMappingsToSiteDir(mappings, "TODO") + )) ++ SiteHelpers.watchSettings(Preprocess) /** * Simple preprocessing of all files in a directory using `@variable@` replacements. */ - def simplePreprocess(sourceDir: File, targetDir: File, cacheFile: File, fileExts: Set[String], replacements: Map[String, String], log: Logger): File = { - transformDirectory(sourceDir, targetDir, hasExtension(fileExts), transformFile(replaceVariable(Variable, replacements ++ defaultReplacements)), cacheFile, log) + private[sbt] def simplePreprocess( + sourceDir: File, + targetDir: File, + cacheFile: File, + fileExts: Set[String], + replacements: Map[String, String], + log: Logger): File = { + transformDirectory( + sourceDir, targetDir, SiteHelpers.hasExtension(fileExts), + SiteHelpers.transformFile(replaceVariable(Variable, replacements ++ defaultReplacements)), + cacheFile, log) } /** Find out what was generated. */ @@ -49,7 +67,13 @@ object PreprocessSupport { /** * Create a transformed version of all files in a directory, given a predicate and a transform function for each file. */ - def transformDirectory(sourceDir: File, targetDir: File, transformable: File => Boolean, transform: (File, File) => Unit, cache: File, log: Logger): File = { + private[sbt] def transformDirectory( + sourceDir: File, + targetDir: File, + transformable: File => Boolean, + transform: (File, File) => Unit, + cache: File, + log: Logger): File = { val runTransform = FileFunction.cached(cache)(FilesInfo.hash, FilesInfo.exists) { (in, out) => val map = Path.rebase(sourceDir, targetDir) if (in.removed.nonEmpty || in.modified.nonEmpty) { @@ -59,44 +83,33 @@ object PreprocessSupport { } val updated = for (source <- in.modified; target <- map(source)) yield { if (source.isFile) { - if (transformable(source)) transform(source, target) - else IO.copyFile(source, target) + if (transformable(source)) { + transform(source, target) + } + else { + IO.copyFile(source, target) + } } target } log.info("Directory preprocessed: " + targetDir) updated - } else Set.empty + } + else { + Set.empty + } } - val sources = (sourceDir ***).get.toSet + val sources = (sourceDir ** AllPassFilter).get.toSet runTransform(sources) targetDir } - - /** - * Check whether a file has one of the given extensions. - */ - def hasExtension(extensions: Set[String])(file: File): Boolean = { - extensions contains file.ext - } - - /** - * Transform a file, line by line. - */ - def transformFile(transform: String => String)(source: File, target: File): Unit = { - IO.reader(source) { reader => - IO.writer(target, "", IO.defaultCharset) { writer => - val pw = new PrintWriter(writer) - IO.foreachLine(reader) { line => pw.println(transform(line)) } - } - } - } - /** * Simple variable replacement in a string. */ - def replaceVariable(regex: Regex, replacements: Map[String, String])(input: String): String = { - def replacement(key: String): String = replacements.getOrElse(key, sys.error("No replacement value defined for: " + key)) + private[sbt] def replaceVariable(regex: Regex, replacements: Map[String, String]) + (input: String): String = { + def replacement(key: String): String = replacements.getOrElse( + key, sys.error("No replacement value defined for: " + key)) regex.replaceAllIn(input, m => replacement(m.group(1))) } } diff --git a/src/main/scala/com/typesafe/sbt/site/Preview.scala b/src/main/scala/com/typesafe/sbt/site/Preview.scala index 0fc440b..55236b4 100644 --- a/src/main/scala/com/typesafe/sbt/site/Preview.scala +++ b/src/main/scala/com/typesafe/sbt/site/Preview.scala @@ -3,12 +3,22 @@ package site import sbt._ import unfiltered.util._ -import SbtSite.SiteKeys._ -object Preview { +object Preview extends AutoPlugin { + override def requires = SbtSite + override def trigger = allRequirements - //@TODO Add configuraiton to make server just local - val settings: Seq[Setting[_]] = Seq( + object autoImport { + val previewSite = TaskKey[Unit]("preview-site", "Launches a jetty server that serves your generated site from the target directory") + val previewFixedPort = SettingKey[Option[Int]]("previewFixedPort") in previewSite + val previewLaunchBrowser = SettingKey[Boolean]("previewLaunchBrowser") in previewSite + } + import SbtSite.autoImport._ + import autoImport._ + + + //@TODO Add configuration to make server just local + override val projectSettings: Seq[Setting[_]] = Seq( previewSite <<= (makeSite, previewFixedPort, previewLaunchBrowser) map { (file, portOption, browser) => val port = portOption getOrElse Port.any val server = createServer(file, port) start() @@ -29,6 +39,6 @@ object Preview { ) def createServer(siteTarget: File, port: Int) = - unfiltered.jetty.Http(port) resources (new URL(siteTarget.toURI.toURL, ".")) + unfiltered.jetty.Http(port) resources new URL(siteTarget.toURI.toURL, ".") } diff --git a/src/main/scala/com/typesafe/sbt/site/SiteHelpers.scala b/src/main/scala/com/typesafe/sbt/site/SiteHelpers.scala new file mode 100644 index 0000000..3e11b04 --- /dev/null +++ b/src/main/scala/com/typesafe/sbt/site/SiteHelpers.scala @@ -0,0 +1,95 @@ +package com.typesafe.sbt.site + +import java.io.PrintWriter + +import sbt.Keys._ +import sbt._ +/** + * Utility/support functions. + */ +object SiteHelpers { + import com.typesafe.sbt.SbtSite.siteMappings + /** Convenience functions to add a task of mappings to a site under a nested directory. */ + def addMappingsToSiteDir( + mappings: TaskKey[Seq[(File, String)]], + nestedDirectory: String): Setting[_] = + siteMappings <++= mappings map { m => + for ((f, d) <- m) yield (f, nestedDirectory + "/" + d) + } + + def selectSubpaths(dir: File, filter: FileFilter): Seq[(File, String)] = + Path.selectSubpaths(dir, filter).toSeq + + def copySite(dir: File, cacheDir: File, maps: Seq[(File, String)]): File = { + val concrete = maps map { case (file, dest) => (file, dir / dest) } + Sync(cacheDir / "make-site")(concrete) + dir + } + + def siteArtifact(name: String) = Artifact(name, Artifact.DocType, "zip", "site") + + def createSiteZip(siteDir: File, zipPath: File, s: TaskStreams): File = { + IO.zip(Path.allSubpaths(siteDir), zipPath) + s.log.info("Site packaged: " + zipPath) + zipPath + } + + def directorySettings(config: Configuration): Seq[Setting[_]] = + inConfig(config)( + Seq( + sourceDirectory := sourceDirectory.value / config.name, + target := target.value / config.name + )) + def watchSettings(config: Configuration): Seq[Setting[_]] = + Seq( + watchSources in Global <++= (sourceDirectory in config) map {_.***.get} + ) + def checkGems(requirements: Map[String, String], s: TaskStreams): Unit = { + /** Generates an error message if the gem's version isn't appropriate for this build. */ + def makeError( + gem: String, + version: String, + current: Option[String]): Option[String] = current match { + case Some(v) if v contains version => None + case Some(v) => Some( + "This build requires the gem [%s (%s)] but found version (%s) instead." format(gem, version, v)) + case None => Some( + "This build requires the gem [%s (%s)] to be installed." format(gem, version)) + } + val errors = for { + (gem, version) <- requirements + error <- makeError(gem, version, getGemVersion(gem)) + } yield error + if (errors.nonEmpty) { + sys.error(errors.mkString("Gem version requirements failed:\n\t", "\n\t", "\n")) + } + } + + /** Checks versions of gems installed. */ + def getGemVersion(gem: String): Option[String] = { + val installed = Seq("gem", "list", "--local", gem).!! + """\((.+)\)""".r.findFirstMatchIn(installed) match { + case None => None + case Some(m) => Some(m.group(1)) + } + } + + /** + * Check whether a file has one of the given extensions. + */ + def hasExtension(extensions: Set[String])(file: File): Boolean = { + extensions contains file.ext + } + + /** + * Transform a file, line by line. + */ + def transformFile(transform: String => String)(source: File, target: File): Unit = { + IO.reader(source) { reader => + IO.writer(target, "", IO.defaultCharset) { writer => + val pw = new PrintWriter(writer) + IO.foreachLine(reader) { line => pw.println(transform(line)) } + } + } + } +} diff --git a/src/main/scala/com/typesafe/sbt/site/SphinxSupport.scala b/src/main/scala/com/typesafe/sbt/site/SphinxSupport.scala index 0097660..8c4162f 100644 --- a/src/main/scala/com/typesafe/sbt/site/SphinxSupport.scala +++ b/src/main/scala/com/typesafe/sbt/site/SphinxSupport.scala @@ -1,60 +1,80 @@ package com.typesafe.sbt package site -import sbt._ -import sbt.Keys._ import com.typesafe.sbt.sphinx._ +import sbt.Keys._ +import sbt._ -object SphinxSupport { - val Sphinx = config("sphinx") +/** Sphinx generator. */ +object SphinxSupport extends AutoPlugin { + override def requires = SbtSite + override def trigger = noTrigger - val sphinxEnv = TaskKey[Map[String, String]]("sphinx-env", "Environment variables to set for forked sphinx-build process.") - val sphinxPackages = SettingKey[Seq[File]]("sphinx-packages", "Custom Python package sources to install for Sphinx.") - val sphinxTags = SettingKey[Seq[String]]("sphinx-tags", "Sphinx tags that should be passed when running Sphinx.") - val sphinxProperties = SettingKey[Map[String, String]]("sphinx-properties", "-D options that should be passed when running Sphinx.") - val sphinxIncremental = SettingKey[Boolean]("sphinx-incremental", "Use incremental Sphinx building. Off by default.") - val sphinxInputs = TaskKey[SphinxInputs]("sphinx-inputs", "Combined inputs for the Sphinx runner.") - val sphinxRunner = TaskKey[SphinxRunner]("sphinx-runner", "The class used to run Sphinx commands.") - val installPackages = TaskKey[Seq[File]]("install-packages", "Install custom Python packages for Sphinx.") - val enableOutput = TaskKey[Boolean]("enable-output", "Enable/disable generation of different outputs.") - val generateHtml = TaskKey[File]("generate-html", "Run Sphinx generation for HTML output.") - val generatePdf = TaskKey[Seq[File]]("generate-pdf", "Run Sphinx generation for PDF output.") - val generateEpub = TaskKey[File]("generate-epub", "Run Sphinx generation for Epub output.") - val generatedHtml = TaskKey[Option[File]]("generated-html", "Sphinx HTML output, if enabled. Enabled by default.") - val generatedPdf = TaskKey[Seq[File]]("generated-pdf", "Sphinx PDF output, if enabled. Disabled by default.") - val generatedEpub = TaskKey[Option[File]]("generated-epub", "Sphinx Epub output, if enabled. Disabled by default") - val generate = TaskKey[File]("sphinx-generate", "Run all enabled Sphinx generation and combine output.") + object autoImport { + val Sphinx = config("sphinx") - def settings(config: Configuration = Sphinx): Seq[Setting[_]] = - Generator.directorySettings(config) ++ - inConfig(config)(Seq( - sphinxPackages := Seq.empty, - sphinxTags := Seq.empty, - sphinxProperties := Map.empty, - sphinxEnv := Map.empty, - sphinxIncremental := false, - includeFilter in generate := AllPassFilter, - excludeFilter in generate := HiddenFileFilter, - sphinxInputs := combineSphinxInputs.value, - sphinxRunner := SphinxRunner(), - installPackages := installPackagesTask.value, - enableOutput in generateHtml := true, - enableOutput in generatePdf := false, - enableOutput in generateEpub := false, - generateHtml <<= generateHtmlTask, - generatePdf <<= generatePdfTask, - generateEpub <<= generateEpubTask, - generatedHtml <<= ifEnabled(generateHtml), - generatedPdf <<= seqIfEnabled(generatePdf), - generatedEpub <<= ifEnabled(generateEpub), - generate <<= generateTask, - includeFilter in Sphinx := AllPassFilter, - mappings <<= mappingsTask, - // For now, we default to passing the version in as a property. - sphinxProperties ++= defaultVersionProperties(version.value), - sphinxEnv <<= defaultEnvTask - )) ++ - Generator.watchSettings(config) // TODO - this may need to be optional. + val sphinxEnv = TaskKey[Map[String, String]]( + "sphinx-env", "Environment variables to set for forked sphinx-build process.") + val sphinxPackages = SettingKey[Seq[File]]( + "sphinx-packages", "Custom Python package sources to install for Sphinx.") + val sphinxTags = SettingKey[Seq[String]]( + "sphinx-tags", "Sphinx tags that should be passed when running Sphinx.") + val sphinxProperties = SettingKey[Map[String, String]]( + "sphinx-properties", "-D options that should be passed when running Sphinx.") + val sphinxIncremental = SettingKey[Boolean]( + "sphinx-incremental", "Use incremental Sphinx building. Off by default.") + val sphinxInputs = TaskKey[SphinxInputs]( + "sphinx-inputs", "Combined inputs for the Sphinx runner.") + val sphinxRunner = TaskKey[SphinxRunner]( + "sphinx-runner", "The class used to run Sphinx commands.") + val installPackages = TaskKey[Seq[File]]( + "install-packages", "Install custom Python packages for Sphinx.") + val enableOutput = TaskKey[Boolean]( + "enable-output", "Enable/disable generation of different outputs.") + val generateHtml = TaskKey[File]("generate-html", "Run Sphinx generation for HTML output.") + val generatePdf = TaskKey[Seq[File]]("generate-pdf", "Run Sphinx generation for PDF output.") + val generateEpub = TaskKey[File]("generate-epub", "Run Sphinx generation for Epub output.") + val generatedHtml = TaskKey[Option[File]]( + "generated-html", "Sphinx HTML output, if enabled. Enabled by default.") + val generatedPdf = TaskKey[Seq[File]]( + "generated-pdf", "Sphinx PDF output, if enabled. Disabled by default.") + val generatedEpub = TaskKey[Option[File]]( + "generated-epub", "Sphinx Epub output, if enabled. Disabled by default") + val generate = TaskKey[File]( + "sphinx-generate", "Run all enabled Sphinx generation and combine output.") + } + import autoImport._ + override def projectSettings: Seq[Setting[_]] = SiteHelpers.directorySettings(Sphinx) ++ + inConfig(Sphinx)( + Seq( + sphinxPackages := Seq.empty, + sphinxTags := Seq.empty, + sphinxProperties := Map.empty, + sphinxEnv := Map.empty, + sphinxIncremental := false, + includeFilter in generate := AllPassFilter, + excludeFilter in generate := HiddenFileFilter, + sphinxInputs := combineSphinxInputs.value, + sphinxRunner := SphinxRunner(), + installPackages := installPackagesTask.value, + enableOutput in generateHtml := true, + enableOutput in generatePdf := false, + enableOutput in generateEpub := false, + generateHtml <<= generateHtmlTask, + generatePdf <<= generatePdfTask, + generateEpub <<= generateEpubTask, + generatedHtml <<= ifEnabled(generateHtml), + generatedPdf <<= seqIfEnabled(generatePdf), + generatedEpub <<= ifEnabled(generateEpub), + generate <<= generateTask, + includeFilter in Sphinx := AllPassFilter, + mappings <<= mappingsTask, + // For now, we default to passing the version in as a property. + sphinxProperties ++= defaultVersionProperties(version.value), + sphinxEnv <<= defaultEnvTask, + SiteHelpers.addMappingsToSiteDir(mappings, "TODO") + )) ++ + SiteHelpers.watchSettings(Sphinx) def defaultEnvTask = installPackages map { pkgs => @@ -62,10 +82,10 @@ object SphinxSupport { "PYTHONPATH" -> Path.makeString(pkgs) ) } - + def defaultVersionProperties(version: String) = { val binV = CrossVersion.binaryVersion(version, "") - Map("version" -> binV, "release" -> version) + Map("version" -> binV, "release" -> version) } def installPackagesTask = Def.task { @@ -103,11 +123,16 @@ object SphinxSupport { runner.generateEpub(inputs, t, s.cacheDirectory, s.log) } - def ifEnabled[T](key: TaskKey[T]): Def.Initialize[Task[Option[T]]] = ifEnabled0[T,Option[T]](key, _ map Some.apply, None) - def seqIfEnabled[T](key: TaskKey[Seq[T]]): Def.Initialize[Task[Seq[T]]] = ifEnabled0[Seq[T], Seq[T]](key, identity, Nil) + def ifEnabled[T](key: TaskKey[T]): Def.Initialize[Task[Option[T]]] = ifEnabled0[T, Option[T]]( + key, _ map Some.apply, None) + def seqIfEnabled[T](key: TaskKey[Seq[T]]): Def.Initialize[Task[Seq[T]]] = ifEnabled0[Seq[T], Seq[T]]( + key, identity, Nil) - private[this] def ifEnabled0[S,T](key: TaskKey[S], f: Task[S] => Task[T], nil: T): Def.Initialize[Task[T]] = (enableOutput in key in key.scope, key.task) flatMap { - (enabled, enabledTask) => if (enabled) f(enabledTask) else task { nil } + private[this] def ifEnabled0[S, T]( + key: TaskKey[S], + f: Task[S] => Task[T], + nil: T): Def.Initialize[Task[T]] = (enableOutput in key in key.scope, key.task) flatMap { + (enabled, enabledTask) => if (enabled) f(enabledTask) else task {nil} } def generateTask = Def.task { @@ -119,7 +144,7 @@ object SphinxSupport { val t = target.value / "docs" val cache = cacheDir / "sphinx" / "docs" val htmlMapping = htmlOutput.toSeq flatMap { html => - html.***.get pair rebase(html, t) + (html ** AllPassFilter).get pair rebase(html, t) } val pdfMapping = pdfOutputs map { pdf => (pdf, t / pdf.name) } val epubMapping = epubOutput.toSeq flatMap { epub => diff --git a/src/main/scala/com/typesafe/sbt/sphinx/SphinxRunner.scala b/src/main/scala/com/typesafe/sbt/sphinx/SphinxRunner.scala index 3ef7226..3590e48 100644 --- a/src/main/scala/com/typesafe/sbt/sphinx/SphinxRunner.scala +++ b/src/main/scala/com/typesafe/sbt/sphinx/SphinxRunner.scala @@ -3,7 +3,7 @@ package sphinx import sbt._ import Keys._ -import SbtSite.SiteKeys.siteMappings + /** * Combined inputs for Sphinx runner.