Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance. Project price only 1 $
You can buy this project and download/modify it how often you want.
/*
* Copyright 2012-2020 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 cats.data.NonEmptySet
import laika.api.config.{ Config, ConfigBuilder, ConfigError, ConfigParser, Origin, Traced }
import laika.ast.Path.Root
import laika.ast.RelativePath.CurrentTree
import laika.ast.RewriteRules.RewriteRulesBuilder
import laika.api.config.Config.IncludeMap
import laika.api.config.ConfigError.TreeErrors
import laika.ast.styles.StyleDeclarationSet
import laika.config.*
import laika.internal.rewrite.TemplateRewriter
import scala.annotation.tailrec
/** A navigatable object is anything that has an associated path.
*/
trait Navigatable {
def path: Path
/** The local name of this navigatable.
*/
lazy val name: String = path.name
}
/** A titled, positional element in the document tree.
*/
sealed trait TreeContent extends Navigatable {
/** The title of this element which can originate from
* the first header of a markup file or from configuration.
*/
def title: Option[SpanSequence]
/** The configuration associated with this element.
*/
def config: Config
/** The position of this element within the document tree.
*/
def position: TreePosition
protected def titleFromConfig: Option[SpanSequence] = {
config.get[Traced[String]](LaikaKeys.title).toOption.flatMap { tracedTitle =>
if (tracedTitle.origin.scope == configScope) {
val title = Seq(Text(tracedTitle.value))
val autonumberConfig = config.get[AutonumberConfig].getOrElse(AutonumberConfig.disabled)
val autonumberEnabled =
autonumberConfig.documents && position.depth < autonumberConfig.maxDepth
if (autonumberEnabled) Some(SpanSequence(position.toSpan +: title))
else Some(SpanSequence(title))
}
else None
}
}
protected def configScope: Origin.Scope
/** Creates the navigation structure for this instance up to the specified depth.
* The returned instance can be used as part of a bigger navigation structure comprising of trees, documents and their sections.
*
* @param context captures the navigation depth, reference path and styles for the navigation tree being built
* @return a navigation item that can be used as part of a bigger navigation structure comprising of trees, documents and their sections
*/
def asNavigationItem(
context: NavigationBuilderContext = NavigationBuilderContext.defaults
): NavigationItem
/** Extracts all runtime messages with the specified minimum level from this tree content.
*/
def runtimeMessages(filter: MessageFilter): Seq[RuntimeMessage]
/** Extracts all invalid elements with the specified minimum message level from this tree content.
*/
def invalidElements(filter: MessageFilter): Seq[Invalid]
/** The formats this tree content should be rendered to.
*/
def targetFormats: TargetFormats = config.get[TargetFormats].getOrElse(TargetFormats.All)
}
/** A template document containing the element tree of a parsed template and its extracted
* configuration section (if present).
*/
class TemplateDocument private (
val path: Path,
val content: TemplateRoot,
val config: ConfigParser
) extends Navigatable {
def withConfig(config: ConfigParser): TemplateDocument =
new TemplateDocument(path, content, config)
/** Applies this template to the specified document, replacing all
* span and block resolvers in the template with the final resolved element.
*/
def applyTo(
document: Document,
rules: RewriteRules,
outputContext: OutputContext
): Either[ConfigError, Document] =
DocumentCursor(document, Some(outputContext))
.flatMap(TemplateRewriter.applyTemplate(_, _ => Right(rules), this))
}
object TemplateDocument {
def apply(path: Path, root: TemplateRoot): TemplateDocument =
new TemplateDocument(path, root, ConfigParser.empty)
}
/** A pure descriptor for a static document, without the actual bytes.
* Used for evaluating links and other AST transformation phases.
*/
case class StaticDocument(path: Path, formats: TargetFormats = TargetFormats.All)
object StaticDocument {
def apply(path: Path, format: String, formats: String*): StaticDocument =
StaticDocument(path, TargetFormats.Selected(NonEmptySet.of(format, formats: _*)))
}
/** A temporary structure usually not exposed to user code.
* It holds a document with an empty Config instance and its actual config
* (obtained from a header section if present) in unresolved form, as it
* needs to be resolved based on a fallback configuration later.
*/
case class UnresolvedDocument(document: Document, config: ConfigParser)
/** Captures information about a document section, without its content.
*/
case class SectionInfo(
id: String,
title: SpanSequence,
content: Seq[SectionInfo],
options: Options = Options.empty
) extends Element with ElementContainer[SectionInfo] {
type Self = SectionInfo
def withOptions(options: Options): SectionInfo = copy(options = options)
/** Creates the navigation structure for this section up to the specified depth.
* The returned instance can be used as part of a bigger navigation structure comprising of documents and trees.
*
* @param context captures the navigation depth, reference path and styles for the navigation tree being built
* @return a navigation item that can be used as part of a bigger navigation structure comprising of trees, documents and their sections
*/
def asNavigationItem(
docPath: Path,
context: NavigationBuilderContext = NavigationBuilderContext.defaults
): NavigationItem = {
val children =
if (context.isComplete) Nil else content.map(_.asNavigationItem(docPath, context.nextLevel))
context.newNavigationItem(title, docPath.withFragment(id), children, TargetFormats.All)
}
}
/** Represents a single document and provides access to the document content and structure
* as well as hooks for triggering rewrite operations.
*
* @param content the tree model obtained from parsing the markup document
* @param fragments separate named fragments that had been extracted from the content
*/
class Document private (
context: TreeNodeContext,
val content: RootElement,
val fragments: Map[String, Element] = Map.empty
) extends DocumentNavigation with TreeContent {
/** The full, absolute path of this document in the (virtual) document tree.
*/
def path: Path = context.path
/** The configuration associated with this document.
*/
def config: Config = context.config
/** Associates the specified config instance with this document.
*
* If you want to add values to the existing configuration of this instance,
* use `modifyConfig` instead, which is more efficient than re-assigning
* a full new instance which is based on the existing one.
*/
def withConfig(config: Config): Document = {
val newContext = context.copy(localConfig = config)
new Document(newContext, content, fragments)
}
/** Modifies the existing config instance for this document by appending
* one or more additional values.
*
* This method is much more efficient than `withConfig` when existing config values should be retained.
*/
def modifyConfig(f: ConfigBuilder => ConfigBuilder): Document = {
val builder = ConfigBuilder.withFallback(context.localConfig)
withConfig(f(builder).build)
}
/** Replaces the entire content of this document with the specified new `RootElement`.
*/
def withContent(content: RootElement): Document =
new Document(context, content, fragments)
/** Replaces the fragments of this document with the specified new map.
*/
def withFragments(newFragments: Map[String, Element]): Document =
new Document(context, content, newFragments)
/** Adds the specified fragments to the existing map of fragments.
*/
def addFragments(newFragments: Map[String, Element]): Document = withFragments(
fragments ++ newFragments
)
/** The position of this document inside a document tree hierarchy, expressed as a list of Ints.
*/
def position: TreePosition = context.position
private[ast] def withPosition(index: TreeNodeIndex): Document =
new Document(context.copy(index = index), content, fragments)
private[laika] def withPosition(index: Int): Document =
withPosition(TreeNodeIndex.Value(index))
private[laika] def withContent(content: RootElement, fragments: Map[String, Element]): Document =
new Document(context, content, fragments)
private[laika] def withTemplateConfig(config: Config): Document =
withConfig(config.withFallback(context.localConfig))
private[ast] def withParent(parent: TreeNodeContext): Document =
new Document(context.copy(parent = Some(parent)), content, fragments)
private[laika] def withPath(path: Path): Document = {
def contextFor(path: Path, oldContext: Option[TreeNodeContext]): TreeNodeContext = {
val oldParent = oldContext.flatMap(_.parent)
val parent = path.parent match {
case Root => oldParent.map(_.copy(localPath = None))
case other => Some(contextFor(other, oldParent))
}
oldContext match {
case Some(existing) => existing.copy(localPath = Some(path.name), parent = parent)
case None => TreeNodeContext(localPath = Some(path.name), parent = parent)
}
}
new Document(
contextFor(path, Some(context)),
content,
fragments
)
}
private def findRoot: Seq[Block] = {
content.collect {
case RootElement(TemplateRoot(_, _) :: Nil, _) => Nil
case RootElement(content, _) => Seq(content)
case EmbeddedRoot(content, _, _) => Seq(content)
}.flatten.headOption.getOrElse(Nil)
}
/** The title of this document, obtained from the document
* structure or from the configuration. In case no title
* is defined in either of the two places the result will
* be `None`.
*/
def title: Option[SpanSequence] = {
def titleFromTree = (RootElement(findRoot) collect { case Title(content, _) =>
SpanSequence(content)
}).headOption
titleFromConfig.orElse(titleFromTree)
}
/** The section structure of this document based on the hierarchy
* of headers found in the original text markup.
*/
lazy val sections: Seq[SectionInfo] = {
def extractSections(blocks: Seq[Block]): Seq[SectionInfo] = {
blocks collect { case Section(Header(_, header, Id(id)), content, _) =>
SectionInfo(id, SpanSequence(header), extractSections(content))
}
}
extractSections(findRoot)
}
def runtimeMessages(filter: MessageFilter): Seq[RuntimeMessage] = filter match {
case MessageFilter.None => Nil
case _ =>
content.collect {
case msg: RuntimeMessage if filter(msg) => msg
}
}
def invalidElements(filter: MessageFilter): Seq[Invalid] = filter match {
case MessageFilter.None => Nil
case _ =>
content.collect {
case inv: Invalid if filter(inv.message) => inv
}
}
/** Returns a new, rewritten document model based on the specified rewrite rules.
*
* If the rule is not defined for a specific element or the rule returns
* a `Retain` action as a result the old element remains in the tree unchanged.
*
* If it returns `Remove` then the node gets removed from the ast,
* if it returns `Replace` with a new element it will replace the old one.
*
* The rewriting is performed bottom-up (depth-first), therefore
* any element container passed to the rule only contains children which have already
* been processed.
*/
def rewrite(rules: RewriteRules): Either[ConfigError, Document] =
DocumentCursor(this).map(_.rewriteTarget(rules))
protected val configScope: Origin.Scope = Origin.DocumentScope
/** Appends the specified content to this tree and return a new instance.
*/
def appendContent(content: Block, contents: Block*): Document = appendContent(content +: contents)
/** Appends the specified content to this tree and return a new instance.
*/
def appendContent(newContent: Seq[Block]): Document =
new Document(context, content.withContent(content.content ++ newContent), fragments)
/** Prepends the specified content to this tree and return a new instance.
*/
def prependContent(content: Block, contents: Block*): Document = prependContent(
content +: contents
)
/** Prepends the specified content to this tree and return a new instance.
*/
def prependContent(newContent: Seq[Block]): Document =
new Document(context, content.withContent(newContent ++ content.content), fragments)
}
object Document {
def apply(path: Path, content: RootElement): Document = {
def contextFor(path: Path): TreeNodeContext = {
val parent = path.parent match {
case Root => None
case other => Some(contextFor(other))
}
TreeNodeContext(localPath = Some(path.name), parent = parent)
}
new Document(contextFor(path), content)
}
}
private[ast] sealed trait TreeNodeIndex
private[ast] object TreeNodeIndex {
case object Unassigned extends TreeNodeIndex
case object Inherit extends TreeNodeIndex
case class Value(index: Int) extends TreeNodeIndex
}
private[ast] case class TreeNodeContext(
localPath: Option[String] = None,
localConfig: Config = Config.empty,
index: TreeNodeIndex = TreeNodeIndex.Unassigned,
parent: Option[TreeNodeContext] = None
) {
lazy val path: Path = parent.map(_.path) match {
case Some(parentPath) => parentPath / localPath.getOrElse("")
case None => localPath.fold[Path](Root)(Root / _)
}
lazy val config: Config = localConfig.withFallback(parent.fold(Config.empty)(_.config))
lazy val position: TreePosition = index match {
case TreeNodeIndex.Value(idx) => parent.fold(TreePosition.root)(_.position).forChild(idx)
case TreeNodeIndex.Inherit => parent.fold(TreePosition.root)(_.position)
case TreeNodeIndex.Unassigned => TreePosition.root
}
def child(localName: String, config: Config = Config.empty): TreeNodeContext =
TreeNodeContext(Some(localName), localConfig = config, parent = Some(this))
}
/** Represents a virtual tree with all its documents, templates, configurations and subtrees.
*
* @param content the markup documents and subtrees except for the (optional) title document
* @param titleDocument the optional title document of this tree
* @param templates all templates on this level of the tree hierarchy that might get applied to a document when it gets rendered
*/
final class DocumentTree private[ast] (
context: TreeNodeContext,
val content: Seq[TreeContent],
val titleDocument: Option[Document] = None,
val templates: Seq[TemplateDocument] = Nil
) extends TreeContent {
/** The full, absolute path of this (virtual) document tree.
*/
def path: Path = context.path
/** The configuration associated with this tree.
*/
def config: Config = context.config
/** The position of this tree inside a document ast hierarchy, expressed as a list of Ints.
*/
def position: TreePosition = context.position
private def withContext(newContext: TreeNodeContext): DocumentTree = {
// separate from copy since context changes need to be propagated to all children
new DocumentTree(
newContext,
content.map {
case d: Document => d.withParent(newContext)
case t: DocumentTree => t.withParent(newContext)
},
titleDocument.map(_.withParent(newContext)),
templates
)
}
private[ast] def setParent(doc: Document): Document = doc.withParent(context)
private def setParent(content: TreeContent): TreeContent = content match {
case d: Document => d.withParent(context)
case t: DocumentTree => t.withParent(context)
}
private[ast] def withContent(
content: Seq[TreeContent] = this.content,
titleDocument: Option[Document] = this.titleDocument
): DocumentTree = new DocumentTree(context, content, titleDocument, templates)
private[laika] def withPosition(index: Int): DocumentTree =
withContext(context.copy(index = TreeNodeIndex.Value(index)))
private[ast] def withParent(parent: TreeNodeContext): DocumentTree =
withContext(context.copy(parent = Some(parent)))
private[laika] def withoutTemplates: DocumentTree =
new DocumentTree(context, content, titleDocument, Nil)
/** Adds the specified document as the title document for this tree,
* replacing the existing title document if present.
*/
def withTitleDocument(doc: Document): DocumentTree =
withContent(titleDocument = Some(setParent(doc)))
/** Adds the specified document as the title document for this tree
* replacing the existing title document if present or, if the parameter is empty,
* removes any existing title document.
*/
def withTitleDocument(doc: Option[Document]): DocumentTree =
withContent(titleDocument = doc.map(setParent))
/** Associates the specified config instance with this document tree.
*
* If you want to add values to the existing configuration of this instance,
* use `modifyConfig` instead, which is more efficient than re-assigning
* a full new instance which is based on the existing one.
*/
def withConfig(config: Config): DocumentTree = withContext(context.copy(localConfig = config))
/** Modifies the existing config instance for this document tree by appending
* one or more additional values.
*
* This method is much more efficient than `withConfig` when existing config values should be retained.
*/
def modifyConfig(f: ConfigBuilder => ConfigBuilder): DocumentTree = {
val builder = ConfigBuilder.withFallback(context.localConfig)
withConfig(f(builder).build)
}
/** Adds the specified template document to this tree,
* retaining any previously added templates.
*/
def addTemplate(template: TemplateDocument): DocumentTree =
new DocumentTree(context, content, titleDocument, templates :+ template)
/** The title of this tree, obtained from configuration.
*/
lazy val title: Option[SpanSequence] = titleDocument.flatMap(_.title).orElse(titleFromConfig)
/** All documents contained in this tree, fetched recursively, depth-first.
*/
lazy val allDocuments: Seq[Document] = {
def collect(tree: DocumentTree): Seq[Document] =
tree.titleDocument.toSeq ++ tree.content.flatMap {
case doc: Document => Seq(doc)
case sub: DocumentTree => collect(sub)
}
collect(this)
}
/** Indicates whether this tree does not contain any markup document.
* Template documents do not count, as they would be ignored in rendering
* when there is no markup document.
*/
lazy val isEmpty: Boolean = {
def nonEmpty(tree: DocumentTree): Boolean = tree.titleDocument.nonEmpty || tree.content.exists {
case _: Document => true
case sub: DocumentTree => nonEmpty(sub)
}
!nonEmpty(this)
}
/** Selects a document from this tree or one of its subtrees by the specified path.
* The path needs to be relative and not point to a parent tree (neither start
* with `/` nor with `..`).
*/
def selectDocument(path: String): Option[Document] = selectDocument(RelativePath.parse(path))
/** Selects a document from this tree or one of its subtrees by the specified path.
* The path must not point to a parent tree (start with `../`)
* as this instance is not aware of its parents.
*/
def selectDocument(path: RelativePath): Option[Document] = path.withoutFragment match {
case CurrentTree / localName =>
(titleDocument.toSeq ++: content).collectFirst {
case d: Document if d.path.name == localName => d
}
case other / localName if path.parentLevels == 0 =>
selectSubtree(other).flatMap(_.selectDocument(localName))
case _ =>
None
}
/** Removes all documents from this tree where the specified filter applies to its path.
* Does not recurse into nested sub-trees and does not apply to templates or title documents.
*/
def removeContent(filter: Path => Boolean): DocumentTree =
withContent(content = content.filterNot(c => filter(c.path)))
/** Appends the specified content to this tree and return a new instance.
*/
def appendContent(content: TreeContent, contents: TreeContent*): DocumentTree = appendContent(
content +: contents
)
/** Appends the specified content to this tree and return a new instance.
*/
def appendContent(newContent: Seq[TreeContent]): DocumentTree =
withContent(content = content ++ newContent.map(setParent))
/** Prepends the specified content to this tree and return a new instance.
*/
def prependContent(content: TreeContent, contents: TreeContent*): DocumentTree = prependContent(
content +: contents
)
/** Prepends the specified content to this tree and return a new instance.
*/
def prependContent(newContent: Seq[TreeContent]): DocumentTree =
withContent(content = newContent.map(setParent) ++ content)
/** Applies the specified function to all elements of the `content` property
* and returns a new document tree.
*
* Applies the function to documents and document trees on this level of the hierarchy.
* IF you want to modify documents recursively, use `modifyDocumentsRecursively` instead.
*/
def modifyContent(f: TreeContent => TreeContent): DocumentTree =
withContent(content = content.map(f).map(setParent))
/** Creates a new tree by applying the specified function to all documents in this tree recursively.
*/
def modifyDocumentsRecursively(f: Document => Document): DocumentTree = {
val newTitle = titleDocument.map(f)
val newContent = content.map {
case d: Document => f(d)
case t: DocumentTree => t.modifyDocumentsRecursively(f)
}
new DocumentTree(context, newContent, newTitle, templates)
}
/** Replaces the contents of this document tree.
* Consider using `modifyContent` instead when only intending to adjust existing content.
*/
def replaceContent(newContent: Seq[TreeContent]): DocumentTree =
withContent(content = newContent.map(setParent))
/** Selects a template from this tree or one of its subtrees by the specified path.
* The path needs to be relative.
*/
def selectTemplate(path: String): Option[TemplateDocument] = selectTemplate(
RelativePath.parse(path)
)
/** Selects a template from this tree or one of its subtrees by the specified path.
* The path must not point to a parent tree (start with `../`)
* as this instance is not aware of its parents.
*/
def selectTemplate(path: RelativePath): Option[TemplateDocument] = path match {
case CurrentTree / localName => templates.find(_.path.name == localName)
case other / localName if path.parentLevels == 0 =>
selectSubtree(other).flatMap(_.selectTemplate(localName))
case _ => None
}
/** Selects the template with the name `default.template.<suffix>` for the
* specified format suffix from this level of the document tree.
*/
def getDefaultTemplate(formatSuffix: String): Option[TemplateDocument] = {
selectTemplate(DefaultTemplatePath.forSuffix(formatSuffix).relative)
}
/** Create a new document tree that contains the specified template as the default.
*/
def withDefaultTemplate(template: TemplateRoot, formatSuffix: String): DocumentTree = {
val defPath = path / DefaultTemplatePath.forSuffix(formatSuffix).relative
val newTemplates = templates.filterNot(_.path == defPath) :+ TemplateDocument(defPath, template)
new DocumentTree(context, content, titleDocument, newTemplates)
}
/** Selects a subtree of this tree by the specified path.
* The path needs to be relative and it may point to a deeply nested
* subtree, not just immediate children.
*/
def selectSubtree(path: String): Option[DocumentTree] = selectSubtree(RelativePath.parse(path))
/** Selects a subtree of this tree by the specified path.
* The path must not point to a parent tree (start with `../`)
* as this instance is not aware of its parents.
*/
def selectSubtree(path: RelativePath): Option[DocumentTree] = path match {
case CurrentTree => Some(this)
case CurrentTree / localName =>
content.collectFirst { case t: DocumentTree if t.path.name == localName => t }
case other / localName if path.parentLevels == 0 =>
selectSubtree(other).flatMap(_.selectSubtree(localName))
case _ => None
}
/** Creates the navigation structure for this tree up to the specified depth.
* The returned instance can be used as part of a bigger navigation structure comprising of trees, documents and their sections.
*
* @param context captures the navigation depth, reference path and styles for the navigation tree being built
* @return a navigation item that can be used as part of a bigger navigation structure comprising of trees, documents and their sections
*/
def asNavigationItem(
context: NavigationBuilderContext = NavigationBuilderContext.defaults
): NavigationItem = {
def hasLinks(item: NavigationItem): Boolean =
item.link.nonEmpty || item.content.exists(hasLinks)
val navContent = content
.filterNot(_.path == context.refPath && context.excludeSelf)
.filterNot(_.config.get[Boolean](LaikaKeys.excludeFromNavigation).getOrElse(false))
val children =
if (context.isComplete) Nil
else navContent.map(_.asNavigationItem(context.nextLevel)).filter(hasLinks)
val navTitle = title.getOrElse(SpanSequence(path.name))
context.newNavigationItem(navTitle, titleDocument, children, targetFormats)
}
def runtimeMessages(filter: MessageFilter): Seq[RuntimeMessage] = filter match {
case MessageFilter.None => Nil
case _ =>
titleDocument.toSeq.flatMap(_.runtimeMessages(filter)) ++ content.flatMap(
_.runtimeMessages(filter)
)
}
def invalidElements(filter: MessageFilter): Seq[Invalid] = filter match {
case MessageFilter.None => Nil
case _ =>
titleDocument.toSeq.flatMap(_.invalidElements(filter)) ++ content.flatMap(
_.invalidElements(filter)
)
}
/** Returns a new tree, with all the document models contained in it
* rewritten based on the specified rewrite rules.
*
* If the rule is not defined for a specific element or the rule returns
* a `Retain` action as a result the old element remains in the tree unchanged.
*
* If it returns `Remove` then the node gets removed from the ast,
* if it returns `Replace` with a new element it will replace the old one.
*
* The rewriting is performed bottom-up (depth-first), therefore
* any element container passed to the rule only contains children which have already
* been processed.
*
* The specified factory function will be invoked for each document contained in this
* tree and must return the rewrite rules for that particular document.
*/
def rewrite(rules: RewriteRulesBuilder): Either[TreeErrors, DocumentTree] =
TreeCursor(this).flatMap(_.rewriteTarget(rules))
protected val configScope: Origin.Scope = Origin.TreeScope
}
object DocumentTree {
/** 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 = new DocumentTree(TreeNodeContext(), 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.
*
* The `styles` property of this type is currently only populated and processed when rendering PDF or XSL-FO.
* Styles for HTML or EPUB documents are part of the `staticDocuments` property instead and will be integrated
* into the final output, but not interpreted.
*
* @param tree the recursive structure of documents, usually obtained from parsing text markup
* @param coverDocument the cover document (usually used with e-book formats like EPUB and PDF)
* @param styles the styles to apply when rendering this tree, only populated for PDF or XSL-FO output
* @param staticDocuments the descriptors for documents that were neither identified as text markup, config or templates, and will be copied as is to the final output
*/
final class DocumentTreeRoot private (
val tree: DocumentTree,
val coverDocument: Option[Document],
val styles: Map[String, StyleDeclarationSet],
val staticDocuments: Seq[StaticDocument],
private[laika] val includes: IncludeMap
) {
private def copy(
tree: DocumentTree = this.tree,
coverDocument: Option[Document] = this.coverDocument,
styles: Map[String, StyleDeclarationSet] = this.styles,
staticDocuments: Seq[StaticDocument] = this.staticDocuments,
includes: IncludeMap = includes
): DocumentTreeRoot = {
new DocumentTreeRoot(tree, coverDocument, styles, staticDocuments, includes)
}
/** The configuration associated with the root of the tree.
*
* Like text markup documents and templates, configurations form a tree
* structure and sub-trees may override and/or add properties that have
* only an effect in that sub-tree.
*/
val config: Config = tree.config
/** The title of this tree, obtained from configuration.
*/
val title: Option[SpanSequence] = tree.title
/** The title document for this tree, if present.
*
* At the root level the title document, if present, will be rendered
* after the cover document.
*/
val titleDocument: Option[Document] = tree.titleDocument
/** All documents contained in this tree, fetched recursively, depth-first.
*/
lazy val allDocuments: Seq[Document] = coverDocument.toSeq ++ tree.allDocuments
/** Indicates whether this tree does not contain any markup document.
* Template documents do not count, as they would be ignored in rendering
* when there is no markup document.
*/
lazy val isEmpty: Boolean = coverDocument.isEmpty && tree.isEmpty
/** Adds the specified document as the cover for this tree,
* replacing the existing cover document if present.
*/
def withCoverDocument(doc: Document): DocumentTreeRoot =
copy(coverDocument = Some(tree.setParent(doc)))
/** Adds the specified document as the cover document for this tree
* replacing the existing cover document if present or, if the parameter is empty,
* removes any existing cover document.
*/
def withCoverDocument(doc: Option[Document]): DocumentTreeRoot =
copy(coverDocument = doc.map(tree.setParent))
/** Adds the specified styles (CSS for PDF) to this document tree.
*/
def addStyles(newStyles: Map[String, StyleDeclarationSet]): DocumentTreeRoot =
copy(styles = styles ++ newStyles)
private[laika] def replaceStaticDocuments(newDocs: Seq[StaticDocument]): DocumentTreeRoot =
copy(staticDocuments = newDocs)
private[laika] def removeStaticDocuments(paths: Set[Path]): DocumentTreeRoot =
copy(staticDocuments = staticDocuments.filterNot(doc => paths.contains(doc.path)))
/** Adds the specified static document references to this document tree.
*/
def addStaticDocuments(newDocs: Seq[StaticDocument]): DocumentTreeRoot =
copy(staticDocuments = staticDocuments ++ newDocs)
private[laika] def addIncludes(newIncludes: IncludeMap): DocumentTreeRoot =
copy(includes = includes ++ newIncludes)
/** Creates a new tree by applying the specified function to all documents in this tree recursively.
*/
def modifyDocumentsRecursively(f: Document => Document): DocumentTreeRoot = {
val newCover = coverDocument.map(f)
val newTree = tree.modifyDocumentsRecursively(f)
copy(coverDocument = newCover, tree = newTree)
}
/** Associates the specified config instance with this document tree.
*
* If you want to add values to the existing configuration of this instance,
* use `modifyConfig` instead, which is more efficient than re-assigning
* a full new instance which is based on the existing one.
*/
def withConfig(config: Config): DocumentTreeRoot = copy(tree = tree.withConfig(config))
/** Modifies the existing config instance for this document tree by appending
* one or more additional values.
*
* This method is much more efficient than `withConfig` when existing config values should be retained.
*/
def modifyConfig(f: ConfigBuilder => ConfigBuilder): DocumentTreeRoot =
copy(tree = tree.modifyConfig(f))
/** Creates a new instance by applying the specified function to the root tree.
*/
def modifyTree(f: DocumentTree => DocumentTree): DocumentTreeRoot = copy(tree = f(tree))
/** Returns a new tree, with all the document models contained in it rewritten based on the specified rewrite rules.
*
* If the rule is not defined for a specific element or the rule returns a `Retain` action as a result
* the old element remains in the tree unchanged.
*
* If it returns `Remove` then the node gets removed from the ast,
* if it returns `Replace` with a new element it will replace the old one.
*
* The rewriting is performed bottom-up (depth-first),
* therefore any element container passed to the rule only contains children which have already been processed.
*
* The specified factory function will be invoked for each document contained in this tree
* and must return the rewrite rules for that particular document.
*/
def rewrite(rules: RewriteRulesBuilder): Either[TreeErrors, DocumentTreeRoot] =
RootCursor(this).flatMap(_.rewriteTarget(rules))
/** Selects and applies the templates contained in this document tree for the specified output format to all documents
* within this tree recursively.
*/
def applyTemplates(
rules: RewriteRulesBuilder,
context: OutputContext
): Either[ConfigError, DocumentTreeRoot] =
TemplateRewriter.applyTemplates(this, rules, context)
/** Selects the configuration associated with a subtree.
* In case the subtree does not exist it will recursively try parent trees
* to reflect the inheritance nature of tree configuration.
* Will always succeed and return the root config if no matching children are found.
*/
@tailrec
def selectTreeConfig(path: Path): Config = tree.selectSubtree(path.relative) match {
case Some(tree) => tree.config
case None if path == Root => config
case None => selectTreeConfig(path.parent)
}
}
object DocumentTreeRoot {
def apply(tree: DocumentTree): DocumentTreeRoot =
new DocumentTreeRoot(
tree,
None,
Map.empty.withDefaultValue(StyleDeclarationSet.empty),
Nil,
Map.empty
)
}