com.lightbend.paradox.markdown.Page.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.{ Forest, Location }
import com.lightbend.paradox.template.PageTemplate
import java.io.File
import java.net.URI
import java.nio.file.{ Path => NioPath, Paths => NioPaths }
import org.pegdown.ast.{ Node, RootNode, SpecialTextNode, TextNode }
import scala.annotation.tailrec
/**
* Common interface for Page and Header, which are linkable.
*/
sealed abstract class Linkable {
def path: String
def label: Node
def group: Option[String]
}
/**
* Header in a page, with anchor path and markdown nodes.
*/
case class Header(path: String, label: Node, group: Option[String], includeIndexes: List[Int]) extends Linkable
/**
* Markdown page with target path, parsed markdown, and headers.
*/
case class Page(file: File, path: String, rootSrcPage: String, label: Node, h1: Header, headers: Forest[Header], markdown: RootNode, group: Option[String], properties: Page.Properties) extends Linkable {
/**
* Path to the root of the site.
*/
val base: String = Path.basePath(path)
/**
* Extract a page title from text nodes in the label.
*/
val title: String = {
import scala.collection.JavaConverters._
def textNodes(node: Node): Seq[String] = {
node.getChildren.asScala.flatMap {
case t: TextNode => Seq(t.getText)
case other => textNodes(other)
}
}
textNodes(label).mkString
}
}
object Page {
/**
* Create a single page from parsed markdown.
*/
def apply(path: String, markdown: RootNode, properties: Map[String, String]): Page = {
apply(path, markdown, identity, properties)
}
/**
* Create a single page from parsed markdown.
*/
def apply(path: String, markdown: RootNode, convertPath: String => String, properties: Map[String, String]): Page = {
convertPage(convertPath)(Index.page(new File(path), path, markdown, properties))
}
/**
* Convert parsed markdown pages into a linked forest of Page objects.
*/
def forest(parsed: Seq[(File, String, RootNode, Map[String, String])], convertPath: String => String, properties: Map[String, String]): Forest[Page] = {
Index.pages(parsed, properties) map (_ map convertPage(convertPath))
}
/**
* Convert an Index.Page into the final Page and Headers.
* The first h1 header is used for the page header and title.
*/
def convertPage(convertPath: String => String)(page: Index.Page): Page = {
// TODO: get default label node from page index link?
val properties = Page.Properties(page.properties)
val targetPath = properties.convertToTarget(convertPath)(page.path)
val rootSrcPage = Path.relativeRootPath(page.file, page.path)
val (h1, subheaders) = page.headers match {
case h :: hs => (Header(h.label.path, h.label.markdown, h.label.group, h.label.includeIndexes), h.children ++ hs)
case Nil => (Header(targetPath, new SpecialTextNode(targetPath), None, Nil), Nil)
}
val headers = subheaders map (_ map (h => Header(h.path, h.markdown, h.group, h.includeIndexes)))
Page(page.file, targetPath, rootSrcPage, h1.label, h1, headers, page.markdown, h1.group, properties)
}
/**
* Collect all page paths.
*/
def allPaths(pages: Forest[Page]): List[String] = {
@tailrec
def collect(location: Option[Location[Page]], paths: List[String] = Nil): List[String] = location match {
case Some(loc) => collect(loc.next, loc.tree.label.path :: paths)
case None => paths
}
pages flatMap { root => collect(Some(root.location)) }
}
/**
* Specific properties at page level for the current page
*/
case class Properties(props: Map[String, String]) {
def get: Map[String, String] = props
/**
* Give the property associated to the key given in input
*/
def apply(property: String, default: String = ""): String = {
props.getOrElse(property, default)
}
/**
* Convert the source file path to the target file path according to the "out" property or not
*/
def convertToTarget(convertPath: String => String): String => String =
(path: String) => replaceFile(props.get(Properties.DefaultOutMdIndicator))(path) getOrElse convertPath(path)
// TODO: give the target suffix ".html" in a more general way
private def replaceFile(prop: Option[String], targetSuffix: String = ".html")(path: String): Option[String] = prop match {
case Some(p) if (p.endsWith(targetSuffix)) => Some(path.dropRight(Path.leaf(path).length) + p)
case _ => None
}
}
object Properties {
val DefaultOutMdIndicator = "out"
val DefaultLayoutMdIndicator = "layout"
}
/**
* Create an included page.
*/
def included(file: File, includeFilePath: String, includedIn: Page, markdown: RootNode): Page = {
val rootSrcPage = Path.relativeRootPath(file, includeFilePath)
Page(file, includedIn.path, rootSrcPage, includedIn.h1.label, includedIn.h1, includedIn.headers, markdown,
includedIn.group, includedIn.properties)
}
}
/**
* Helper methods for paths.
*/
object Path {
/**
* Form a relative path to the root, based on the number of directories in a path.
*/
def basePath(path: String): String = {
"../" * path.count(_ == '/')
}
/**
* Resolve a relative path against a base path.
*/
def resolve(base: String, path: String): String = {
new URI(base).resolve(path).getPath
}
/**
* Replace the file extension in a path.
*/
def replaceExtension(from: String, to: String)(link: String): String = {
val uri = new URI(link)
replaceSuffix(from, to)(uri.getPath) + Option(uri.getFragment).fold("")("#".+)
}
/**
* Replace the suffix of a path.
*/
def replaceSuffix(from: String, to: String)(path: String): String = {
if (path.endsWith(from)) path.dropRight(from.length) + to else path
}
/**
* Provide the leaf (file) from a path
*/
def leaf(path: String): String = {
path.split('/').reverse.head
}
/**
* Normalize the path to Unix style root path. Removes drive letter and appends the "/" symbol.
* Also converts backslashes to slashes.
*/
def toUnixStyleRootPath(pathString: String): String = toUnixStyleRootPath(NioPaths.get(pathString))
/**
* Normalize the path to Unix style root path. Removes drive letter and appends the "/" symbol.
* Also converts backslashes to slashes.
*/
def toUnixStyleRootPath(path: NioPath): String = {
val fullPathWithDriveLetter = path.toAbsolutePath
val fullPathString =
if (fullPathWithDriveLetter.getRoot ne null)
File.separator + fullPathWithDriveLetter.getRoot.relativize(fullPathWithDriveLetter).toString
else
fullPathWithDriveLetter.toString
fullPathString.replace('\\', '/')
}
/**
* Provide the relative root path from a local path related to a full path
*/
def relativeRootPath(file: File, localPath: String): String = {
val pathString = toUnixStyleRootPath(file.toPath)
if (pathString.endsWith(localPath)) pathString.dropRight(localPath.length) else pathString
}
/**
* Provide the local path given the root path and the full path
*/
def relativeLocalPath(rootPath: String, fullPath: String): String = {
val root = new URI(toUnixStyleRootPath(rootPath))
val full = new URI(toUnixStyleRootPath(fullPath))
root.relativize(full).toString
}
/**
* Provide the final target file given a particular source file/link
*/
def generateTargetFile(localPath: String, globalPageMappings: Map[String, String]): String => String = {
val mappings = relativeMapping(localPath, globalPageMappings)
{ link =>
val uri = new URI(localPath).resolve(new URI(link))
mappings.get(uri.getPath) match {
case Some(p) => p + Option(uri.getFragment).fold("")("#".+)
case None => sys.error(s"No reference link corresponding to $link")
}
}
}
/**
* Provide the mappings "source to target" files relative to the current file path given the root mappings
*/
def relativeMapping(localPath: String, globalPageMappings: Map[String, String]): Map[String, String] = {
def parentsPath(path: String): List[String] = path.split('/').toList.reverse.tail.reverse
val rootPath = parentsPath(localPath)
globalPageMappings map { mapping =>
val rootMap = (parentsPath(mapping._1), parentsPath(mapping._2))
mapping._1 -> (refRelativePath(rootPath, rootMap._2, leaf(mapping._2)))
}
}
/**
* Provide the modified path relative to the root path
*/
def refRelativePath(root: List[String], path: List[String], leafFile: String): String = {
def listPath(root: List[String], path: List[String]): List[String] = (root, path) match {
case (Nil, ps) => ps
case (rs, Nil) => rs map (_ => "..")
case (r :: rs, p :: ps) if (r == p) => listPath(rs, ps)
case _ => root.map(_ => "..") ::: path
}
(listPath(root, path) ::: List(leafFile)).mkString("/")
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy