Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

sbt plugin & preview server: allow to add custom renderers #588

Merged
merged 14 commits into from
Mar 29, 2024
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 29 additions & 2 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@ import laika.format.Markdown.GitHubFlavor
import laika.config.SyntaxHighlighting
import sbt.Keys.crossScalaVersions
import org.scalajs.linker.interface.ESVersion
import com.typesafe.tools.mima.core.{
ProblemFilters,
MissingClassProblem,
ReversedMissingMethodProblem,
DirectMissingMethodProblem,
MissingTypesProblem
}
import Dependencies._

inThisBuild(
Expand Down Expand Up @@ -157,7 +164,18 @@ lazy val preview = project.in(file("preview"))
.dependsOn(core.jvm, io % "compile->compile;test->test", pdf)
.settings(
name := "laika-preview",
libraryDependencies ++= (http4s :+ munit)
libraryDependencies ++= (http4s :+ munit),
mimaBinaryIssueFilters ++= Seq(
ProblemFilters.exclude[ReversedMissingMethodProblem](
"laika.preview.ServerConfig.binaryRenderers"
),
ProblemFilters.exclude[ReversedMissingMethodProblem](
"laika.preview.ServerConfig.withBinaryRenderers"
),
ProblemFilters.exclude[DirectMissingMethodProblem]("laika.preview.ServerConfig#Impl.this"),
ProblemFilters.exclude[DirectMissingMethodProblem]("laika.preview.ServerConfig#Impl.apply"),
ProblemFilters.exclude[MissingTypesProblem]("laika.preview.ServerConfig$Impl$")
)
)

lazy val plugin = project.in(file("sbt"))
Expand All @@ -179,5 +197,14 @@ lazy val plugin = project.in(file("sbt"))
io / publishLocal,
pdf / publishLocal,
preview / publishLocal
).evaluated
).evaluated,
mimaBinaryIssueFilters ++= Seq(
ProblemFilters.exclude[MissingClassProblem]("laika.sbt.Tasks$OutputFormat"),
ProblemFilters.exclude[MissingClassProblem]("laika.sbt.Tasks$OutputFormat$"),
ProblemFilters.exclude[MissingClassProblem]("laika.sbt.Tasks$OutputFormat$AST$"),
ProblemFilters.exclude[MissingClassProblem]("laika.sbt.Tasks$OutputFormat$EPUB$"),
ProblemFilters.exclude[MissingClassProblem]("laika.sbt.Tasks$OutputFormat$HTML$"),
ProblemFilters.exclude[MissingClassProblem]("laika.sbt.Tasks$OutputFormat$PDF$"),
ProblemFilters.exclude[MissingClassProblem]("laika.sbt.Tasks$OutputFormat$XSLFO$")
)
)
34 changes: 34 additions & 0 deletions docs/src/02-running-laika/01-sbt-plugin.md
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,40 @@ By default, the port is 4242, the poll interval is 3 seconds.
With the `verbose` options the console will log all pages served.


### Installing Additional Renderers

The `laikaRenderers` setting holds the configurations for all of Laika's built-in renderers
and enables users to install their own or 3rd-party renderers.

An example for installing a custom `indexFormat` renderer:

```scala mdoc:compile-only
import laika.ast.Path.Root
import laika.api.format.*
import laika.io.config.BinaryRendererConfig

def indexFormat: TwoPhaseRenderFormat[Formatter, BinaryPostProcessor.Builder] = ???

val artifact = laika.io.config.Artifact(
basePath = Root / "search",
suffix = "dat"
jenshalm marked this conversation as resolved.
Show resolved Hide resolved
)

laikaRenderers += BinaryRendererConfig(
alias = "index",
format = indexFormat,
artifact = artifact,
includeInSite = true,
supportsSeparations = false
)
```

With the configuration above, the search index can be generated by calling `laikaGenerate index`
(or together with other formats in one transformation - e.g. `laikaGenerate html index`).

When the includeInSite property is true, the index will also be generated when calling `laikaSite`.
jenshalm marked this conversation as resolved.
Show resolved Hide resolved


### Inspecting Laika's Configuration

Run `show laikaDescribe` to get a formatted summary of the active configuration,
Expand Down
74 changes: 74 additions & 0 deletions io/src/main/scala/laika/io/config/Artifact.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* Copyright 2012-2024 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.config

import laika.ast.Path

/** Represents an artifact with optional classifiers such as those used by binary renderers.
* The format is `<basePath><classifiers>.<suffix>` where `classifiers` is the empty string
* if the `classifiers` property is empty. Otherwise `classifiers` will be concatenated
* with `-` as separator and prefix.
*/
sealed abstract class Artifact private[config] {

/** The basePath for the artifact within the virtual tree, including the file name without suffix.
*/
def basePath: Path

/** The classifiers to insert between basePath and suffix, with `-` as separator and prefix.
*/
def classifiers: Seq[String]

/** The file suffix of the artifact.
*/
def suffix: String

/** Returns a new Artifact with the specified classifiers applied.
*/
def withClassifiers(cls: Seq[String]): Artifact

/** The full virtual Path for this Artifact, including all applied classifiers.
*/
def fullPath: Path = {
val classifierString = if (classifiers.isEmpty) "" else classifiers.mkString("-", "-", "")
basePath.parent / (basePath.name + classifierString + "." + suffix)
}

}

object Artifact {

private final case class Impl(
basePath: Path,
suffix: String,
classifiers: Seq[String]
) extends Artifact {
override def productPrefix = "Artifact"

def withClassifiers(cls: Seq[String]): Artifact = Impl(basePath, suffix, cls)
}

def apply(
basePath: Path,
suffix: String
): Artifact = Impl(
basePath,
suffix,
Nil
)

}
124 changes: 124 additions & 0 deletions io/src/main/scala/laika/io/config/RendererConfig.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/*
* Copyright 2012-2024 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.config

import laika.api.format.{ BinaryPostProcessor, RenderFormat, TwoPhaseRenderFormat }

/** Base trait for the configuration of renderers where the execution is not directly triggered by the user.
* Examples for such a scenario are the preview server and the sbt plugin
*/
sealed abstract class RendererConfig private[config] {

/** The alias that triggers the execution of this renderer when invoked from a command line.
*
* In the context of Laika's sbt plugin this would be in the format `laikaGenerate <alias>`.
*/
def alias: String

/** Indicates whether this renderer should be executed alongside the HTML renderer when a site is generated.
*
* In the context of Laika's sbt plugin this means when `true`` the renderer will execute when `laikaSite` is invoked,
* and if `false` the renderer can only be invoked explicitly by using `laikaGenerate <alias>`.
*/
def includeInSite: Boolean

}

/** Represents the configuration for a text renderer (of type `laika.api.format.RenderFormat`)
* to be used with the `laikaGenerate` and `laikaSite` tasks.
*/
sealed abstract class TextRendererConfig private extends RendererConfig {

/** The render format to be used with this renderer.
*/
def format: RenderFormat[?]
}

/** Represents the configuration for a binary renderer
* (of type `laika.api.format.TwoPhaseRenderFormat`)
* to be used with the `laikaGenerate` and `laikaSite` tasks.
*/
sealed abstract class BinaryRendererConfig private extends RendererConfig {

/** The binary render format to be used with this renderer.
*/
def format: TwoPhaseRenderFormat[?, BinaryPostProcessor.Builder]

/** The artifact to be produced by this renderer.
*/
def artifact: Artifact

/** Indicates whether multiple different versions of the output with different
* classifiers in their name should be written in case the user
* has used the `@:select` directive in the input sources.
*
* For details about this directive, see
* [[https://typelevel.org/Laika/latest/07-reference/01-standard-directives.html#select Select Directive]]
* in the manual.
*/
def supportsSeparations: Boolean
}

object TextRendererConfig {

private final case class Impl(
alias: String,
format: RenderFormat[?],
includeInSite: Boolean
) extends TextRendererConfig {
override def productPrefix = "TextRendererConfig"
}

def apply(
alias: String,
format: RenderFormat[?],
includeInSite: Boolean
): TextRendererConfig = Impl(
alias,
format,
includeInSite
)

}

object BinaryRendererConfig {

private final case class Impl(
alias: String,
format: TwoPhaseRenderFormat[?, BinaryPostProcessor.Builder],
artifact: Artifact,
includeInSite: Boolean,
supportsSeparations: Boolean
) extends BinaryRendererConfig {
override def productPrefix = "BinaryRendererConfig"
}

def apply(
alias: String,
format: TwoPhaseRenderFormat[?, BinaryPostProcessor.Builder],
artifact: Artifact,
includeInSite: Boolean,
supportsSeparations: Boolean
): BinaryRendererConfig = Impl(
alias,
format,
artifact,
includeInSite,
supportsSeparations
)

}
Loading