com.lightbend.paradox.markdown.Index.scala Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of paradox_2.10 Show documentation
Show all versions of paradox_2.10 Show documentation
Paradox is a markdown documentation tool for software projects.
The newest version!
/*
* Copyright © 2015 - 2019 Lightbend, Inc.
*
* 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 com.lightbend.paradox.markdown
import com.lightbend.paradox.tree.Tree
import com.lightbend.paradox.tree.Tree.Forest
import java.io.File
import org.pegdown.ast._
import scala.annotation.tailrec
import scala.collection.JavaConverters._
/**
* Create index of pages from parsed markdown.
*/
object Index {
/**
* @param level
* @param path
* @param markdown
* @param group
* @param includeIndexes If this header came from an included file, this has the index of the include file,
* starting from the top level page include, down to the deepest nesting.
*/
case class Ref(level: Int, path: String, markdown: Node, group: Option[String], includeIndexes: List[Int])
case class Page(file: File, path: String, markdown: RootNode, properties: Map[String, String], indices: Forest[Ref], headers: Forest[Ref])
def pages(parsed: Seq[(File, String, RootNode, Map[String, String])], properties: Map[String, String]): Forest[Page] = {
link(parsed.map((page _).tupled).toList, properties)
}
/**
* Create a new Index.Page with parsed indices and headers.
*/
def page(file: File, path: String, markdown: RootNode, properties: Map[String, String]): Page =
Page(file, path, markdown, properties, indices(markdown), headers(markdown))
/**
* Create a tree of header refs from a parsed markdown page.
*/
def headers(root: RootNode): Forest[Ref] = {
Tree.hierarchy(headerRefs(root, group = None, includeIndexes = Nil))(Ordering[Int].on[Ref](_.level))
}
/**
* Extract refs from markdown headers.
*/
private def headerRefs(root: RootNode, group: Option[String], includeIndexes: List[Int]): List[Ref] = {
root.getChildren.asScala.toList.flatMap {
case header: HeaderNode =>
header.getChildren.asScala.toList.flatMap {
case anchor: AnchorLinkSuperNode => List(Ref(header.getLevel, "#" + anchor.name, anchor.contents, group, includeIndexes))
case anchor: AnchorLinkNode => List(Ref(header.getLevel, "#" + anchor.getName, new TextNode(anchor.getText), group, includeIndexes))
case _ => Nil
}
case node: DirectiveNode if node.format == DirectiveNode.Format.ContainerBlock =>
// TODO check whether my assumption that Container DirectiveNode's always contain RootNode's holds,
// if so maybe move that cast to DirectiveNode
val newGroup = node.attributes.classes().asScala.find(_.startsWith("group-")).map(_.substring("group-".size))
headerRefs(node.contentsNode.asInstanceOf[RootNode], newGroup, includeIndexes)
case node @ IncludeNode(included, _, _) =>
headerRefs(included, group, includeIndexes :+ node.getStartIndex)
case _ => Nil
}
}
/**
* Create a tree of page refs from index directives in a parsed markdown page.
*/
def indices(root: RootNode): Forest[Ref] = {
Tree.hierarchy(indexRefs(root))(Ordering[Int].on[Ref](_.level))
}
/**
* Extract refs from 'index' directives.
*/
private def indexRefs(root: RootNode): List[Ref] = {
root.getChildren.asScala.toList.flatMap {
case node: DirectiveNode if isIndexDirective(node) => listedRefs(node)
case _ => Nil
}
}
/**
* Determine whither this is an index directive, by name and format.
*/
private def isIndexDirective(node: DirectiveNode): Boolean = {
node.format == DirectiveNode.Format.ContainerBlock && node.name == "index"
}
/**
* Extract refs from list items. Increment level at each list item.
*/
private def listedRefs(node: Node, level: Int = 1): List[Ref] = {
node.getChildren.asScala.toList.flatMap {
case li: ListItemNode => linkRef(li, level).toList ++ listedRefs(li, level + 1)
case other => listedRefs(other, level)
}
}
/**
* Extract ref from nearest explicit link node.
*/
@tailrec
private def linkRef(node: Node, level: Int): Option[Ref] = {
node match {
case link: ExpLinkNode => Some(Ref(level, link.url, link.getChildren.get(0), group = None, Nil))
case other => other.getChildren.asScala.toList match {
// only check first children
case first :: _ => linkRef(first, level)
case _ => None
}
}
}
/**
* Link together pages into trees using parsed indices.
*/
def link(pages: List[Page], properties: Map[String, String]): Forest[Page] = {
// Substitute all variables in index page links first
val substituted = pages.map { page =>
page.copy(indices = page.indices.map {
case Tree.Node(label, children) =>
val newPath = Writer.substituteVarsInString(label.path, properties ++ page.properties)
Tree.Node(label.copy(path = newPath), children)
case other => other
})
}
Tree.link(substituted, links(substituted))
}
/**
* Exception thrown for unknown pages in index links.
*/
class LinkException(message: String) extends RuntimeException(message)
/**
* Find links between pages using parsed indices.
*/
def links(pages: List[Page]): Map[Page, List[Page]] = {
import scala.collection.mutable
val edges = mutable.Map.empty[Page, List[Page]].withDefaultValue(Nil)
val pageMap = (pages map { page => page.path -> page }).toMap
def lookup(current: String, path: String) = {
pageMap.get(Path.resolve(current, path)).getOrElse {
throw new LinkException(s"Unknown page [$path] linked from [$current]")
}
}
def add(path: String, page: Page, indices: Forest[Ref], nested: Boolean): Unit = {
// if nested then prepending children, so process this level in reverse to retain order
(if (nested) indices.reverse else indices) foreach { i =>
val child = lookup(path, i.label.path)
val current = edges(page)
// nested links have priority (being further up the overall hierarchy)
val added = if (nested) child :: current else current ::: List(child)
edges += page -> added
add(path, child, i.children, nested = true)
}
}
pages foreach { page => add(page.path, page, page.indices, nested = false) }
edges.toMap.withDefaultValue(Nil)
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy