Skip to content

Commit

Permalink
Merge latest 0.19 and remove new deprecations (#464)
Browse files Browse the repository at this point in the history
* fix CSS for Helium UI-components for setups diverging from the defaults (#456)
* render class attribute as render hint for top navigation bar
* render class attribute as render hint for landing page
  based on approximate perceptual luminance of configured gradient colors
* amend Helium CSS variable generator to produce new scoped mappings
* adjust Helium CSS for new CSS variable mappings
* fix css for ui-components on landing page
* adjust transparent overlay for inverted color mode
* hover for landing page button that works in all modes

* Update sbt, scripted-plugin to 1.9.1 in 0.19.x (#458)
* Update scalafmt-core to 3.7.7 in 0.19.x (#459)
* Update cats-effect to 3.5.1 in 0.19.x (#460)

* link validation - more config options and better defaults (#432)
* add new LinkValidation config types
* switch config default to LinkValidation.Local for core transformer

* introduce DocumentTree.builder (#436)

* extract DocumentTreeBuilder from parser runtime in laika-io
* DocumentTreeBuilder - scaladoc and deprecations
* DocumentTreeBuilder - add new spec that tests new API directly

* remove functionality deprecated in merged 0.19 branch
  • Loading branch information
jenshalm authored Jul 9, 2023
1 parent e7407ff commit c7eca83
Show file tree
Hide file tree
Showing 69 changed files with 1,170 additions and 578 deletions.
2 changes: 1 addition & 1 deletion .scalafmt.conf
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
version = 3.7.4
version = 3.7.7

runner.dialect = scala212

Expand Down
2 changes: 1 addition & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ lazy val docs = project.in(file("docs"))
Laika / target := baseDirectory.value / "target",
mdocIn := baseDirectory.value / "src",
mdocVariables := Map(
"LAIKA_VERSION" -> "0.19.2"
"LAIKA_VERSION" -> "0.19.3"
),
mdocExtraArguments := Seq("--no-link-hygiene"),
scalacOptions ~= disableUnusedWarningsForMdoc
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,14 +64,8 @@ class ReStructuredTextToHTMLSpec extends FunSuite {
}

def transformAndCompare(name: String): Unit = {
val noValidation =
"""{%
|laika.links.excludeFromValidation = ["/"]
|%}
|""".stripMargin

val path = FileIO.classPathResourcePath("/rstSpec") + "/" + name
val input = noValidation + FileIO.readFile(path + ".rst")
val input = FileIO.readFile(path + ".rst")

def quotedBlockContent(content: Seq[Block], attr: Seq[Span]) =
if (attr.isEmpty) content
Expand Down
6 changes: 3 additions & 3 deletions core/shared/src/main/scala/laika/ast/Cursor.scala
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import laika.config.{
}
import laika.parse.SourceFragment
import laika.rewrite.{ OutputContext, ReferenceResolver }
import laika.rewrite.link.{ LinkConfig, LinkValidator, TargetValidation }
import laika.rewrite.link.{ LinkConfig, LinkValidation, LinkValidator, TargetValidation }
import laika.rewrite.nav.{
AutonumberConfig,
ConfigurablePathTranslator,
Expand Down Expand Up @@ -169,14 +169,14 @@ object RootCursor {

def validate(doc: Document): Option[DocumentConfigErrors] = List(
doc.config.getOpt[Boolean](LaikaKeys.versioned).toEitherNec,
doc.config.getOpt[Boolean](LaikaKeys.validateLinks).toEitherNec,
doc.config.getOpt[String](LaikaKeys.title).toEitherNec,
doc.config.getOpt[TargetFormats].toEitherNec
).parSequence.left.toOption.map(DocumentConfigErrors.apply(doc.path, _))

def validateRoot: Seq[DocumentConfigErrors] = List(
target.config.getOpt[String](LaikaKeys.siteBaseURL).toEitherNec,
target.config.getOpt[LinkConfig].toEitherNec
target.config.getOpt[LinkConfig].toEitherNec,
target.config.getOpt[LinkValidation].toEitherNec
).parSequence.fold(errs => Seq(DocumentConfigErrors(Root, errs)), _ => Nil)

val validations = NonEmptyChain
Expand Down
275 changes: 275 additions & 0 deletions core/shared/src/main/scala/laika/ast/DocumentTreeBuilder.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,275 @@
/*
* Copyright 2012-2023 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.ast

import laika.config.{ Config, ConfigError, ConfigParser, Origin }
import cats.implicits.*
import laika.config.Config.IncludeMap
import laika.config.Origin.{ DocumentScope, TreeScope }
import laika.rewrite.nav.TitleDocumentConfig

import scala.collection.mutable

/** API for a safe and concise way of constructing a `DocumentTree`.
*
* The hierarchy of the tree will be constructed based on the provided `Path` instances
* while also ensuring that document configuration is wired up correctly (inheriting
* the configuration from directories).
*
* @author Jens Halm
*/
class DocumentTreeBuilder private[laika] (parts: List[DocumentTreeBuilder.BuilderPart] = Nil) {

import laika.collection.TransitionalCollectionOps._
import DocumentTreeBuilder._

private lazy val distinctParts: List[BuilderPart] = {
/* distinctBy does not exist in 2.12
insertion happens at head, so later additions are dropped and the final result is reversed
this is cheaper than using `ListMap` which has O(n) insertion.
*/
val paths = mutable.Set[Path]()
parts.filter(p => paths.add(p.path)).reverse
}

private def extract(
content: Seq[TreeContent],
docName: String
): (Option[Document], Seq[TreeContent]) = {
val extracted = content.collectFirst { case d: Document if d.path.basename == docName => d }
val filteredContent =
if (extracted.nonEmpty) content.filterNot(_.path.basename == docName) else content
(extracted, filteredContent)
}

private def buildNode(path: Path, content: Seq[BuilderPart]): TreePart = {

val treeContent = content.collect {
case tree: TreePart => tree
case markup: MarkupPart => markup
case doc: DocumentPart => doc
}
val templates = content.collect { case TemplatePart(doc) => doc }

val hoconConfig = content.collect { case c: HoconPart => c }
val treeConfig = content.collect { case c: ConfigPart => c }

TreePart(path, treeContent, templates, hoconConfig, treeConfig)
}

private def resolveAndBuildDocument(
doc: UnresolvedDocument,
baseConfig: Config,
includes: IncludeMap
): Either[ConfigError, Document] =
doc.config.resolve(Origin(DocumentScope, doc.document.path), baseConfig, includes).map(config =>
doc.document.copy(config = config)
)

private def applyBaseConfig(doc: Document, baseConfig: Config): Document =
doc.copy(config =
doc.config.withFallback(baseConfig).withOrigin(Origin(DocumentScope, doc.path))
)

private def resolveAndBuildTree(
result: TreePart,
baseConfig: Config,
includes: IncludeMap
): Either[ConfigError, DocumentTree] = {

val resolvedConfig =
result.hocon.foldLeft[Either[ConfigError, Config]](Right(result.mergeConfigs(baseConfig))) {
case (acc, unresolved) =>
acc.flatMap(base =>
unresolved.config.resolve(Origin(TreeScope, unresolved.path), base, includes)
)
}

for {
treeConfig <- resolvedConfig
titleName <- TitleDocumentConfig.inputName(treeConfig)
resolvedContent <- result.content.toVector.traverse {
case tree: TreePart => resolveAndBuildTree(tree, treeConfig, includes)
case markup: MarkupPart => resolveAndBuildDocument(markup.doc, treeConfig, includes)
case doc: DocumentPart => Right(applyBaseConfig(doc.doc, treeConfig))
}
} yield {
val (title, content) = extract(resolvedContent, titleName)
DocumentTree(result.path, content, title, result.templates, treeConfig)
}
}

private def buildTree(
result: TreePart,
baseConfig: Config,
defaultTitleName: String
): DocumentTree = {

val treeConfig = result.mergeConfigs(baseConfig)
val titleName = TitleDocumentConfig.inputName(treeConfig).getOrElse(defaultTitleName)
val allContent = result.content.flatMap {
case tree: TreePart => Some(buildTree(tree, treeConfig, titleName))
case doc: DocumentPart => Some(applyBaseConfig(doc.doc, treeConfig))
case _: MarkupPart => None
}
val (title, content) = extract(allContent, titleName)
DocumentTree(result.path, content, title, result.templates, treeConfig)
}

private def collectStyles(parts: Seq[BuilderPart]): Map[String, StyleDeclarationSet] = parts
.collect { case StylePart(styleSet, format) => (format, styleSet) }
.groupBy(_._1)
.mapValuesStrict(
_
.map(_._2)
.sortBy(set => (set.precedence, set.paths.headOption.fold("")(_.toString)))
.reduce(_ ++ _)
)
.withDefaultValue(StyleDeclarationSet.empty)

/** Internal entry point for the parser runtime in laika-io.
* Placed here as it shares a lot of functionality with the public builder API,
* but using additional sub-types of `BuilderPart` for parsed, but yet unresolved
* documents and configurations.
*/
private[laika] def resolveAndBuildRoot(
baseConfig: Config,
includes: IncludeMap
): Either[ConfigError, DocumentTreeRoot] = {
val tree = TreeBuilder.build(distinctParts, buildNode)
val styles = collectStyles(distinctParts)
for {
resolvedTree <- resolveAndBuildTree(tree, baseConfig, includes)
} yield {
val (cover, content) = extract(resolvedTree.content, "cover")
val rootTree = resolvedTree.copy(content = content)
DocumentTreeRoot(rootTree, cover, styles, includes = includes)
}
}

private def addParts(newParts: List[BuilderPart]) =
new DocumentTreeBuilder(newParts.reverse ++ parts)

/** Add the specified documents to the builder.
* Existing instances with identical paths will be overridden.
*/
def addDocuments(docs: List[Document]): DocumentTreeBuilder =
addParts(docs.map(DocumentPart(_)))

/** Add the specified document to the builder.
* Existing instances with identical paths will be overridden.
*/
def addDocument(doc: Document): DocumentTreeBuilder =
new DocumentTreeBuilder(DocumentPart(doc) +: parts)

/** Add the specified tree configuration to the builder.
* The path it will be assigned to will be taken from the `origin`
* property of the `Config` instance.
* Existing instances with identical paths will be overridden.
*
* For assigning a configuration to a specific document and not
* an entire tree or subtree, set the `config` property of
* a `Document` instance directly before adding it to the builder.
*/
def addConfig(config: Config): DocumentTreeBuilder =
new DocumentTreeBuilder(ConfigPart(config.origin.path, config) +: parts)

/** Add the specified templates to the builder.
* Existing instances with identical paths will be overridden.
*/
def addTemplates(docs: List[TemplateDocument]): DocumentTreeBuilder =
addParts(docs.map(TemplatePart(_)))

/** Add the specified template to the builder.
* Existing instances with identical paths will be overridden.
*/
def addTemplate(doc: TemplateDocument): DocumentTreeBuilder =
new DocumentTreeBuilder(TemplatePart(doc) +: parts)

/** Builds a `DocumentTreeRoot` from the provided instances and wires the
* configuration of documents to that of parent trees for proper inheritance.
*/
def buildRoot: DocumentTreeRoot = buildRoot(Config.empty)

/** Builds a `DocumentTreeRoot` from the provided instances, using the specified
* `Config` instance as a base for the configuration of all trees and documents.
* Also wires configuration of documents to that of parent trees for proper inheritance.
*/
def buildRoot(baseConfig: Config): DocumentTreeRoot = {
val tree = build(baseConfig)
val (cover, content) = extract(tree.content, "cover")
DocumentTreeRoot(tree.copy(content = content), cover)
}

/** Builds a `DocumentTree` from the provided instances and wires the
* configuration of documents to that of parent trees for proper inheritance.
*/
def build: DocumentTree = build(Config.empty)

/** Builds a `DocumentTree` from the provided instances, using the specified
* `Config` instance as a base for the configuration of all trees and documents.
* Also wires configuration of documents to that of parent trees for proper inheritance.
*/
def build(baseConfig: Config): DocumentTree = {
val tree = TreeBuilder.build(distinctParts, buildNode)
buildTree(tree, baseConfig, TitleDocumentConfig.defaultInputName)
}

}

private[laika] object DocumentTreeBuilder {

sealed trait BuilderPart extends Navigatable

sealed trait TreeContentPart extends BuilderPart

case class DocumentPart(doc: Document) extends TreeContentPart {
val path: Path = doc.path
}

case class MarkupPart(doc: UnresolvedDocument) extends TreeContentPart {
val path: Path = doc.document.path
}

case class TemplatePart(doc: TemplateDocument) extends BuilderPart {
val path: Path = doc.path
}

case class StylePart(doc: StyleDeclarationSet, format: String) extends BuilderPart {
val path: Path = doc.paths.head
}

case class HoconPart(path: Path, config: ConfigParser) extends BuilderPart

case class ConfigPart(path: Path, config: Config) extends BuilderPart

case class TreePart(
path: Path,
content: Seq[TreeContentPart],
templates: Seq[TemplateDocument],
hocon: Seq[HoconPart],
config: Seq[ConfigPart]
) extends TreeContentPart {

def mergeConfigs(baseConfig: Config): Config =
config.foldLeft(baseConfig) { case (acc, conf) =>
conf.config.withFallback(acc).withOrigin(Origin(TreeScope, conf.path))
}

}

}
23 changes: 23 additions & 0 deletions core/shared/src/main/scala/laika/ast/documents.scala
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,16 @@
package laika.ast

import cats.data.NonEmptySet
import laika.ast.Path.Root
import laika.ast.RelativePath.CurrentTree
import laika.ast.RewriteRules.RewriteRulesBuilder
import laika.config.Config.IncludeMap
import laika.config._
import laika.rewrite.nav.{ AutonumberConfig, TargetFormats }
import laika.rewrite.{ DefaultTemplatePath, OutputContext, TemplateRewriter }

import scala.runtime.AbstractFunction6

/** A navigatable object is anything that has an associated path.
*/
trait Navigatable {
Expand Down Expand Up @@ -528,6 +531,26 @@ case class DocumentTree(

}

object DocumentTree
extends AbstractFunction6[
Path,
Seq[TreeContent],
Option[Document],
Seq[TemplateDocument],
Config,
TreePosition,
DocumentTree
] {
// TODO - simplify for 1.x - signature is required to satisfy mima (companion introduced in 0.19.3)

/** A new, empty builder for constructing a new `DocumentTree`.
*/
val builder = new DocumentTreeBuilder()

/** An empty `DocumentTree` without any documents, templates or configurations. */
val empty: DocumentTree = DocumentTree(Root, Nil)
}

/** Represents the root of a tree of documents. In addition to the recursive structure of documents,
* usually obtained by parsing text markup, it holds additional items like styles and static documents,
* which may contribute to the rendering of a site or an e-book.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,7 @@ object ExtensionBundle {
override val slugBuilder: Option[String => String] = Some(SlugBuilder.default)

override val baseConfig: Config =
ConfigBuilder.empty.withValue("laika.version", "0.19.3-SNAPSHOT").build
ConfigBuilder.empty.withValue("laika.version", "0.19.3").build

override val parsers: ParserBundle = ParserBundle(
styleSheetParser = Some(CSSParsers.styleDeclarationSet)
Expand Down
2 changes: 0 additions & 2 deletions core/shared/src/main/scala/laika/config/LaikaKeys.scala
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,6 @@ object LaikaKeys {

val targetFormats: Key = root.child("targetFormats")

val validateLinks: Key = root.child("validateLinks")

val firstHeaderAsTitle: Key = root.child("firstHeaderAsTitle")

val artifactBaseName: Key = root.child("artifactBaseName")
Expand Down
Loading

0 comments on commit c7eca83

Please sign in to comment.