diff --git a/core/src/main/java/org/pegdown/ast/ActiveLinkNode.java b/core/src/main/java/org/pegdown/ast/ClassyLinkNode.java similarity index 80% rename from core/src/main/java/org/pegdown/ast/ActiveLinkNode.java rename to core/src/main/java/org/pegdown/ast/ClassyLinkNode.java index e40eb9c7..7635cbcd 100644 --- a/core/src/main/java/org/pegdown/ast/ActiveLinkNode.java +++ b/core/src/main/java/org/pegdown/ast/ClassyLinkNode.java @@ -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; } diff --git a/core/src/main/scala/com/lightbend/paradox/ParadoxProcessor.scala b/core/src/main/scala/com/lightbend/paradox/ParadoxProcessor.scala index 64307e81..758b34bd 100644 --- a/core/src/main/scala/com/lightbend/paradox/ParadoxProcessor.scala +++ b/core/src/main/scala/com/lightbend/paradox/ParadoxProcessor.scala @@ -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 @@ -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") @@ -49,6 +51,9 @@ 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) => @@ -56,9 +61,7 @@ class ParadoxProcessor(reader: Reader = new Reader, writer: Writer = new Writer) 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) @@ -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 @@ -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 @@ -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) diff --git a/core/src/main/scala/com/lightbend/paradox/markdown/ActiveLink.scala b/core/src/main/scala/com/lightbend/paradox/markdown/ClassyLink.scala similarity index 81% rename from core/src/main/scala/com/lightbend/paradox/markdown/ActiveLink.scala rename to core/src/main/scala/com/lightbend/paradox/markdown/ClassyLink.scala index ad238c68..ba8ec5f0 100644 --- a/core/src/main/scala/com/lightbend/paradox/markdown/ActiveLink.scala +++ b/core/src/main/scala/com/lightbend/paradox/markdown/ClassyLink.scala @@ -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"""""") + case link: ClassyLinkNode => + printer.print(s"""""") link.getChildren.asScala.foreach(_.accept(visitor)) printer.print("") true diff --git a/core/src/main/scala/com/lightbend/paradox/markdown/Index.scala b/core/src/main/scala/com/lightbend/paradox/markdown/Index.scala index 2d28c14f..ae137a70 100644 --- a/core/src/main/scala/com/lightbend/paradox/markdown/Index.scala +++ b/core/src/main/scala/com/lightbend/paradox/markdown/Index.scala @@ -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) diff --git a/core/src/main/scala/com/lightbend/paradox/markdown/TableOfContents.scala b/core/src/main/scala/com/lightbend/paradox/markdown/TableOfContents.scala index 5628c2dd..0a3bac1f 100644 --- a/core/src/main/scala/com/lightbend/paradox/markdown/TableOfContents.scala +++ b/core/src/main/scala/com/lightbend/paradox/markdown/TableOfContents.scala @@ -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) } /** @@ -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) } /** @@ -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) } } @@ -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) } } diff --git a/core/src/main/scala/com/lightbend/paradox/markdown/Writer.scala b/core/src/main/scala/com/lightbend/paradox/markdown/Writer.scala index f1ba8b24..aad4e13b 100644 --- a/core/src/main/scala/com/lightbend/paradox/markdown/Writer.scala +++ b/core/src/main/scala/com/lightbend/paradox/markdown/Writer.scala @@ -109,7 +109,7 @@ object Writer { } def defaultPlugins(context: Context): Seq[ToHtmlSerializerPlugin] = Seq( - new ActiveLinkSerializer, + new ClassyLinkSerializer, new AnchorLinkSerializer, new DirectiveSerializer(defaultDirectives(context)) ) diff --git a/core/src/test/scala/com/lightbend/paradox/markdown/IndexSpec.scala b/core/src/test/scala/com/lightbend/paradox/markdown/IndexSpec.scala index bbc715a0..602e8d9b 100644 --- a/core/src/test/scala/com/lightbend/paradox/markdown/IndexSpec.scala +++ b/core/src/test/scala/com/lightbend/paradox/markdown/IndexSpec.scala @@ -92,7 +92,8 @@ class IndexSpec extends MarkdownBaseSpec { |@@@ index | - [b](b.md) | - [c](c.md) - | - [d](d.md) + | - [d](d.md) + | - [h](h.md) |@@@ """, "b.md" -> """ @@ -100,6 +101,7 @@ class IndexSpec extends MarkdownBaseSpec { |@@@ index | - [e](e.md) | - [f](f.md) + | - [g](g.md) |@@@ """, "c.md" -> """ @@ -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 """) } diff --git a/core/src/test/scala/com/lightbend/paradox/markdown/NavigationSpec.scala b/core/src/test/scala/com/lightbend/paradox/markdown/NavigationSpec.scala new file mode 100644 index 00000000..af50338c --- /dev/null +++ b/core/src/test/scala/com/lightbend/paradox/markdown/NavigationSpec.scala @@ -0,0 +1,295 @@ +/* + * Copyright © 2015 - 2017 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 } + +class NavigationSpec extends MarkdownBaseSpec { + + val site = Location.forest(pages( + "index.md" -> """ + |@@@ index + |* [1](1.md) + |* [2](2.md) + | - [a](2/a.md) + | - [b](2/b.md) + |* [3](3.md) + | - [a](3/a.md) + | + [i](3/a/i.md) + | + [ii](3/a/ii.md) + | - [b](3/b.md) + | + [i](3/b/i.md) + | + [ii](3/b/ii.md) + |@@@ + """, + "1.md" -> """ + |# 1 + """, + "2.md" -> """ + |# 2 + """, + "2/a.md" -> """ + |# 2/a + """, + "2/b.md" -> """ + |# 2/b + """, + "3.md" -> """ + |# 3 + """, + "3/a.md" -> """ + |# 3/a + """, + "3/a/i.md" -> """ + |# 3/a/i + """, + "3/a/ii.md" -> """ + |# 3/a/ii + """, + "3/b.md" -> """ + |# 3/b + """, + "3/b/i.md" -> """ + |# 3/b/i + |## A + |### B + |## C + |### D + """, + "3/b/ii.md" -> """ + |# 3/b/ii + |## A + |### B + |## C + |### D + """ + )) + + "TableOfContents" should "create full navigation including everything" in { + navigation( + new TableOfContents(pages = true, headers = true, ordered = false, maxDepth = 6, maxExpandDepth = None), + site + ) shouldEqual html(""" + | + """) + } + + it should "create navigation for pages up to max depth" in { + navigation( + new TableOfContents(pages = true, headers = false, ordered = false, maxDepth = 2, maxExpandDepth = None), + site + ) shouldEqual html(""" + | + """) + } + + it should "create auto-expanding navigation for pages up to max depths (at level one)" in { + navigation( + new TableOfContents(pages = true, headers = false, ordered = false, maxDepth = 1, maxExpandDepth = Some(1)), + site + ) shouldEqual html(""" + | + """) + } + + it should "create auto-expanding navigation for pages up to max depths (at level two)" in { + navigation( + new TableOfContents(pages = true, headers = false, ordered = false, maxDepth = 1, maxExpandDepth = Some(1)), + site.get.rightmostChild + ) shouldEqual html(""" + | + """) + } + + it should "create auto-expanding navigation for pages up to max depths (at level three)" in { + navigation( + new TableOfContents(pages = true, headers = false, ordered = false, maxDepth = 1, maxExpandDepth = Some(1)), + site.get.rightmostChild.get.rightmostChild + ) shouldEqual html(""" + | + """) + } + + it should "create auto-expanding navigation for pages up to max depths (at level four)" in { + navigation( + new TableOfContents(pages = true, headers = false, ordered = false, maxDepth = 1, maxExpandDepth = Some(1)), + site.get.rightmostChild.get.rightmostChild.get.rightmostChild + ) shouldEqual html(""" + | + """) + } + + it should "create auto-expanding navigation for pages and headers up to max depths (at level four)" in { + navigation( + new TableOfContents(pages = true, headers = true, ordered = false, maxDepth = 1, maxExpandDepth = Some(1)), + site.get.rightmostChild.get.rightmostChild.get.rightmostChild + ) shouldEqual html(""" + | + """) + } + + it should "create auto-expanding navigation for ancestor pages only if expand depth is 0" in { + navigation( + new TableOfContents(pages = true, headers = false, ordered = false, maxDepth = 1, maxExpandDepth = Some(0)), + site.get.rightmostChild.get.rightmostChild + ) shouldEqual html(""" + | + """) + } + + def navigation(toc: TableOfContents, location: Option[Location[Page]])(implicit context: Location[Page] => Writer.Context = writerContext): String = { + location match { + case Some(loc) => normalize(markdownWriter.writeToc(toc.root(loc), context(loc))) + case None => "" + } + } + +} diff --git a/core/src/test/scala/com/lightbend/paradox/markdown/TocDirectiveSpec.scala b/core/src/test/scala/com/lightbend/paradox/markdown/TocDirectiveSpec.scala index 1a5c9399..2ef9c577 100644 --- a/core/src/test/scala/com/lightbend/paradox/markdown/TocDirectiveSpec.scala +++ b/core/src/test/scala/com/lightbend/paradox/markdown/TocDirectiveSpec.scala @@ -39,16 +39,16 @@ class TocDirectiveSpec extends MarkdownBaseSpec { |

Foo

|
| AA

A

@@ -33,42 +33,42 @@

A<

Next is the TOC.

Followed by the index of a select number of pages.

diff --git a/plugin/src/sbt-test/paradox/site/expected/a/a.html b/plugin/src/sbt-test/paradox/site/expected/a/a.html index 76f35188..d681080c 100644 --- a/plugin/src/sbt-test/paradox/site/expected/a/a.html +++ b/plugin/src/sbt-test/paradox/site/expected/a/a.html @@ -15,17 +15,17 @@
  • AA
  • A AB diff --git a/plugin/src/sbt-test/paradox/site/expected/a/b.html b/plugin/src/sbt-test/paradox/site/expected/a/b.html index ecbaa37a..f5fdb62c 100644 --- a/plugin/src/sbt-test/paradox/site/expected/a/b.html +++ b/plugin/src/sbt-test/paradox/site/expected/a/b.html @@ -16,17 +16,17 @@
  • AB
  • AA AC diff --git a/plugin/src/sbt-test/paradox/site/expected/a/c.html b/plugin/src/sbt-test/paradox/site/expected/a/c.html index fb4170ee..9f751db4 100644 --- a/plugin/src/sbt-test/paradox/site/expected/a/c.html +++ b/plugin/src/sbt-test/paradox/site/expected/a/c.html @@ -16,17 +16,17 @@
  • AC
  • AB BA diff --git a/plugin/src/sbt-test/paradox/site/expected/b/a.html b/plugin/src/sbt-test/paradox/site/expected/b/a.html index 940e4155..596cba4c 100644 --- a/plugin/src/sbt-test/paradox/site/expected/b/a.html +++ b/plugin/src/sbt-test/paradox/site/expected/b/a.html @@ -15,17 +15,17 @@
  • BA
  • AC BAA diff --git a/plugin/src/sbt-test/paradox/site/expected/b/a/a.html b/plugin/src/sbt-test/paradox/site/expected/b/a/a.html index 192f96ab..1c0615ce 100644 --- a/plugin/src/sbt-test/paradox/site/expected/b/a/a.html +++ b/plugin/src/sbt-test/paradox/site/expected/b/a/a.html @@ -16,17 +16,17 @@
  • BAA
  • BA BB diff --git a/plugin/src/sbt-test/paradox/site/expected/b/b.html b/plugin/src/sbt-test/paradox/site/expected/b/b.html index 51095c22..21530523 100644 --- a/plugin/src/sbt-test/paradox/site/expected/b/b.html +++ b/plugin/src/sbt-test/paradox/site/expected/b/b.html @@ -16,17 +16,17 @@
  • BB
  • BAA CA diff --git a/plugin/src/sbt-test/paradox/site/expected/c/a.html b/plugin/src/sbt-test/paradox/site/expected/c/a.html index 2028a0ff..e66cb859 100644 --- a/plugin/src/sbt-test/paradox/site/expected/c/a.html +++ b/plugin/src/sbt-test/paradox/site/expected/c/a.html @@ -15,17 +15,17 @@
  • CA
  • BB

    CA