Skip to content

Commit

Permalink
Merge pull request #114 from pvlugter/auto-expand-navigation
Browse files Browse the repository at this point in the history
Auto-expanding navigation
  • Loading branch information
pvlugter authored May 12, 2017
2 parents f71a5c3 + 5376280 commit 45cf0c3
Show file tree
Hide file tree
Showing 43 changed files with 634 additions and 131 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,17 @@
import java.util.List;

/**
* An active link is an explicit link with an 'active' class attribute.
* Explicit link with class attribute.
*/
public class ActiveLinkNode extends AbstractNode {
public class ClassyLinkNode extends AbstractNode {

public final String href;
public final String classAttribute;
public final Node child;

public ActiveLinkNode(String href, Node child) {
public ClassyLinkNode(String href, String classAttribute, Node child) {
this.href = href;
this.classAttribute = classAttribute;
this.child = child;
}

Expand Down
21 changes: 12 additions & 9 deletions core/src/main/scala/com/lightbend/paradox/ParadoxProcessor.scala
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import com.lightbend.paradox.template.PageTemplate
import com.lightbend.paradox.markdown.{ Breadcrumbs, Groups, Page, Path, Reader, TableOfContents, Writer, Frontin, PropertyUrl, Url }
import com.lightbend.paradox.tree.Tree.{ Forest, Location }
import java.io.File
import org.pegdown.ast.{ ActiveLinkNode, ExpLinkNode, RootNode }
import org.pegdown.ast.{ ClassyLinkNode, ExpLinkNode, RootNode }
import org.stringtemplate.v4.STErrorListener
import scala.annotation.tailrec

Expand All @@ -40,7 +40,9 @@ class ParadoxProcessor(reader: Reader = new Reader, writer: Writer = new Writer)
targetSuffix: String,
groups: Map[String, Seq[String]],
properties: Map[String, String],
navigationDepth: Int,
navDepth: Int,
navExpandDepth: Option[Int],
navIncludeHeaders: Boolean,
pageTemplate: PageTemplate,
errorListener: STErrorListener): Seq[(File, String)] = {
require(!groups.values.flatten.map(_.toLowerCase).groupBy(identity).values.exists(_.size > 1), "Group names may not overlap")
Expand All @@ -49,16 +51,17 @@ class ParadoxProcessor(reader: Reader = new Reader, writer: Writer = new Writer)
val paths = Page.allPaths(pages).toSet
val globalPageMappings = rootPageMappings(pages)

val navToc = new TableOfContents(pages = true, headers = navIncludeHeaders, ordered = false, maxDepth = navDepth, maxExpandDepth = navExpandDepth)
val pageToc = new TableOfContents(pages = false, headers = true, ordered = false, maxDepth = navDepth)

@tailrec
def render(location: Option[Location[Page]], rendered: Seq[(File, String)] = Seq.empty): Seq[(File, String)] = location match {
case Some(loc) =>
val page = loc.tree.label
val pageProperties = properties ++ page.properties.get
val currentMapping = Path.generateTargetFile(Path.relativeLocalPath(page.rootSrcPage, page.file.getPath), globalPageMappings)_
val writerContext = Writer.Context(loc, paths, currentMapping, sourceSuffix, targetSuffix, groups, pageProperties)
val pageToc = new TableOfContents(pages = true, headers = false, ordered = false, maxDepth = navigationDepth)
val headerToc = new TableOfContents(pages = false, headers = true, ordered = false, maxDepth = navigationDepth)
val pageContext = PageContents(leadingBreadcrumbs, groups, loc, writer, writerContext, pageToc, headerToc)
val pageContext = PageContents(leadingBreadcrumbs, groups, loc, writer, writerContext, navToc, pageToc)
val outputFile = new File(outputDirectory, page.path)
outputFile.getParentFile.mkdirs
pageTemplate.write(page.properties(Page.Properties.DefaultLayoutMdIndicator, pageTemplate.defaultName), pageContext, outputFile, errorListener)
Expand All @@ -71,7 +74,7 @@ class ParadoxProcessor(reader: Reader = new Reader, writer: Writer = new Writer)
/**
* Default template contents for a markdown page at a particular location.
*/
case class PageContents(leadingBreadcrumbs: List[(String, String)], groups: Map[String, Seq[String]], loc: Location[Page], writer: Writer, context: Writer.Context, pageToc: TableOfContents, headerToc: TableOfContents) extends PageTemplate.Contents {
case class PageContents(leadingBreadcrumbs: List[(String, String)], groups: Map[String, Seq[String]], loc: Location[Page], writer: Writer, context: Writer.Context, navToc: TableOfContents, pageToc: TableOfContents) extends PageTemplate.Contents {
import scala.collection.JavaConverters._

private val page = loc.tree.label
Expand All @@ -85,10 +88,10 @@ class ParadoxProcessor(reader: Reader = new Reader, writer: Writer = new Writer)
lazy val getSelf = link(Some(loc))
lazy val getNext = link(loc.next)
lazy val getBreadcrumbs = writer.writeBreadcrumbs(Breadcrumbs.markdown(leadingBreadcrumbs, loc.path), context)
lazy val getNavigation = writer.writeNavigation(pageToc.root(loc), context)
lazy val getNavigation = writer.writeNavigation(navToc.root(loc), context)
lazy val getGroups = Groups.html(groups)
lazy val hasSubheaders = page.headers.nonEmpty
lazy val getToc = writer.writeToc(headerToc.headers(loc), context)
lazy val getToc = writer.writeToc(pageToc.headers(loc), context)
lazy val getSource_url = githubLink(Some(loc)).getHtml

lazy val getProperties = context.properties.asJava
Expand All @@ -109,7 +112,7 @@ class ParadoxProcessor(reader: Reader = new Reader, writer: Writer = new Writer)

private def link(location: Location[Page]): String = {
val node = if (active(location))
new ActiveLinkNode(href(location), location.tree.label.label)
new ClassyLinkNode(href(location), "active", location.tree.label.label)
else
new ExpLinkNode("", href(location), location.tree.label.label)
writer.writeFragment(node, context)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,12 @@ import org.pegdown.Printer
import scala.collection.JavaConverters._

/**
* Serialize an ActiveLinkNode, adding the active class attribute.
* Serialize a ClassyLink, adding the class attribute.
*/
class ActiveLinkSerializer extends ToHtmlSerializerPlugin {
class ClassyLinkSerializer extends ToHtmlSerializerPlugin {
def visit(node: Node, visitor: Visitor, printer: Printer): Boolean = node match {
case link: ActiveLinkNode =>
printer.print(s"""<a href="${link.href}" class="active">""")
case link: ClassyLinkNode =>
printer.print(s"""<a href="${link.href}" class="${link.classAttribute}">""")
link.getChildren.asScala.foreach(_.accept(visitor))
printer.print("</a>")
true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,8 @@ object Index {
}

def add(path: String, page: Page, indices: Forest[Ref], nested: Boolean): Unit = {
indices foreach { i =>
// 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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,34 +23,34 @@ import org.pegdown.ast._
/**
* Create markdown list for table of contents on a page.
*/
class TableOfContents(pages: Boolean = true, headers: Boolean = true, ordered: Boolean = true, maxDepth: Int = 6) {
class TableOfContents(pages: Boolean = true, headers: Boolean = true, ordered: Boolean = true, maxDepth: Int = 6, maxExpandDepth: Option[Int] = None) {

/**
* Create a TOC bullet list for a Page.
*/
def markdown(location: Location[Page]): Node = {
markdown(location.tree.label.base, location.tree.label.path, location.tree)
markdown(location.tree.label.base, Some(location), location.tree)
}

/**
* Create a TOC bullet list for a TOC at a certain point within the section hierarchy.
*/
def markdown(location: Location[Page], tocIndex: Int): Node = {
markdown(location.tree.label.base, location.tree.label.path, nested(location.tree, tocIndex))
markdown(location.tree.label.base, Some(location), nested(location.tree, tocIndex))
}

/**
* Create a TOC bullet list for a Page tree, given the base and active paths.
* Create a TOC bullet list for a Page tree, given the base path and active location.
*/
def markdown(base: String, active: String, tree: Tree[Page]): Node = {
subList(base, active, tree, depth = 0).getOrElse(list(Nil))
def markdown(base: String, active: Option[Location[Page]], tree: Tree[Page]): Node = {
subList(base, active, tree, depth = 0, expandDepth = None).getOrElse(list(Nil))
}

/**
* Create a TOC bullet list from the root location.
*/
def root(location: Location[Page]): Node = {
markdown(location.tree.label.base, location.tree.label.path, location.root.tree)
markdown(location.tree.label.base, Some(location), location.root.tree)
}

/**
Expand All @@ -59,7 +59,7 @@ class TableOfContents(pages: Boolean = true, headers: Boolean = true, ordered: B
def headers(location: Location[Page]): Node = {
val page = location.tree.label
val tree = Tree.leaf(page.copy(headers = List(Tree(page.h1, page.headers))))
markdown(base = page.base, active = "", tree)
markdown(base = page.base, active = None, tree)
}

/**
Expand All @@ -83,14 +83,14 @@ class TableOfContents(pages: Boolean = true, headers: Boolean = true, ordered: B
case None => (0, Nil)
}

private def subList[A <: Linkable](base: String, active: String, tree: Tree[A], depth: Int): Option[Node] = {
private def subList[A <: Linkable](base: String, active: Option[Location[Page]], tree: Tree[A], depth: Int, expandDepth: Option[Int]): Option[Node] = {
tree.label match {
case page: Page =>
val subHeaders = if (headers) items(base + page.path, active, page.headers, depth) else Nil
val subPages = if (pages) items(base, active, tree.children, depth) else Nil
val subHeaders = if (headers) items(base + page.path, active, page.headers, depth, expandDepth) else Nil
val subPages = if (pages) items(base, active, tree.children, depth, expandDepth) else Nil
optList(subHeaders ::: subPages)
case header: Header =>
val subHeaders = if (headers) items(base, active, tree.children, depth) else Nil
val subHeaders = if (headers) items(base, active, tree.children, depth, expandDepth) else Nil
optList(subHeaders)
}
}
Expand All @@ -106,22 +106,37 @@ class TableOfContents(pages: Boolean = true, headers: Boolean = true, ordered: B
else new BulletListNode(parent)
}

private def items[A <: Linkable](base: String, active: String, forest: Forest[A], depth: Int): List[Node] = {
forest map item(base, active, depth + 1)
private def items[A <: Linkable](base: String, active: Option[Location[Page]], forest: Forest[A], depth: Int, expandDepth: Option[Int]): List[Node] = {
forest map item(base, active, depth + 1, expandDepth.map(_ + 1))
}

private def item[A <: Linkable](base: String, active: String, depth: Int)(tree: Tree[A]): Node = {
private def item[A <: Linkable](base: String, active: Option[Location[Page]], depth: Int, expandDepth: Option[Int])(tree: Tree[A]): Node = {
val linkable = tree.label
val label = link(base, linkable.path, linkable.label, active)
val label = link(base, linkable, active)
val parent = new SuperNode
parent.getChildren.add(label)
if (depth < maxDepth) subList(base, active, tree, depth).foreach(parent.getChildren.add)
val autoExpandDepth = autoExpand(linkable, active, expandDepth)
if ((depth < maxDepth) || autoExpandDepth.isDefined)
subList(base, active, tree, depth, autoExpandDepth).foreach(parent.getChildren.add)
new ListItemNode(parent)
}

private def link(base: String, path: String, label: Node, active: String): Node = {
if (path == active) new ActiveLinkNode(base + path, label)
else new ExpLinkNode("", base + path, label)
private def autoExpand[A <: Linkable](linkable: A, active: Option[Location[Page]], expandDepth: Option[Int]): Option[Int] = {
maxExpandDepth flatMap { max =>
expandDepth.filter(_ < max) orElse // currently expanding and still below max
(if (active.exists(_.path.drop(1).exists(_.tree.label == linkable))) Some(max) else None) orElse // expand ancestors of the active page
(if ((max > 0) && active.exists(_.tree.label == linkable)) Some(0) else None) // expand from the active page
}
}

private def link[A <: Linkable](base: String, linkable: A, active: Option[Location[Page]]): Node = {
val (path, classAttribute) = linkable match {
case page: Page =>
val isActive = active.exists(_.tree.label.path == page.path)
(if (headers && isActive) (page.path + page.h1.path) else page.path, if (isActive) "active page" else "page")
case header: Header => (header.path, "header")
}
new ClassyLinkNode(base + path, classAttribute, linkable.label)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ object Writer {
}

def defaultPlugins(context: Context): Seq[ToHtmlSerializerPlugin] = Seq(
new ActiveLinkSerializer,
new ClassyLinkSerializer,
new AnchorLinkSerializer,
new DirectiveSerializer(defaultDirectives(context))
)
Expand Down
14 changes: 12 additions & 2 deletions core/src/test/scala/com/lightbend/paradox/markdown/IndexSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -92,14 +92,16 @@ class IndexSpec extends MarkdownBaseSpec {
|@@@ index
| - [b](b.md)
| - [c](c.md)
| - [d](d.md)
| - [d](d.md)
| - [h](h.md)
|@@@
""",
"b.md" -> """
|# B
|@@@ index
| - [e](e.md)
| - [f](f.md)
| - [g](g.md)
|@@@
""",
"c.md" -> """
Expand All @@ -113,14 +115,22 @@ class IndexSpec extends MarkdownBaseSpec {
""",
"f.md" -> """
|# F
""",
"g.md" -> """
|# G
""",
"h.md" -> """
|# H
""") shouldEqual index(
"""
|- a.html
| - b.html
| - c.html
| - d.html
| - e.html
| - f.html
| - d.html
| - g.html
| - h.html
""")
}

Expand Down
Loading

0 comments on commit 45cf0c3

Please sign in to comment.