diff --git a/core/src/main/scala/com/lightbend/paradox/ParadoxProcessor.scala b/core/src/main/scala/com/lightbend/paradox/ParadoxProcessor.scala
index 61f7bfc6..8ab289fa 100644
--- a/core/src/main/scala/com/lightbend/paradox/ParadoxProcessor.scala
+++ b/core/src/main/scala/com/lightbend/paradox/ParadoxProcessor.scala
@@ -20,11 +20,13 @@ import com.lightbend.paradox.template.PageTemplate
import com.lightbend.paradox.markdown._
import com.lightbend.paradox.tree.Tree.{ Forest, Location }
import java.io.{ File, FileOutputStream, OutputStreamWriter }
+import java.util
-import org.pegdown.ast.{ ClassyLinkNode, ExpLinkNode, RootNode }
+import org.pegdown.ast._
import org.stringtemplate.v4.STErrorListener
import scala.annotation.tailrec
+import scala.collection.JavaConverters._
/**
* Markdown site processor.
@@ -175,20 +177,63 @@ class ParadoxProcessor(reader: Reader = new Reader, writer: Writer = new Writer)
* Parse markdown files (with paths) into a forest of linked pages.
*/
def parsePages(mappings: Seq[(File, String)], convertPath: String => String, properties: Map[String, String]): Forest[Page] = {
- Page.forest(parseMarkdown(mappings), convertPath, properties)
+ Page.forest(parseMarkdown(mappings, properties), convertPath, properties)
}
/**
* Parse markdown files into pegdown AST.
*/
- def parseMarkdown(mappings: Seq[(File, String)]): Seq[(File, String, RootNode, Map[String, String])] = {
+ def parseMarkdown(mappings: Seq[(File, String)], properties: Map[String, String]): Seq[(File, String, RootNode, Map[String, String])] = {
mappings map {
case (file, path) =>
val frontin = Frontin(file)
- (file, normalizePath(path), reader.read(frontin.body), frontin.header)
+ val root = parseAndProcessMarkdown(file, frontin.body, properties ++ frontin.header)
+ (file, normalizePath(path), root, frontin.header)
}
}
+ def parseAndProcessMarkdown(file: File, markdown: String, properties: Map[String, String]): RootNode = {
+ val root = reader.read(markdown)
+ processIncludes(file, root, properties)
+ }
+
+ private def processIncludes(file: File, root: RootNode, properties: Map[String, String]): RootNode = {
+ val newRoot = new RootNode
+ // This is a mutable list, and is expected to be mutated by anything that wishes to add children
+ val newChildren = newRoot.getChildren
+
+ root.getChildren.asScala.foreach {
+ case include: DirectiveNode if include.name == "include" =>
+ val labels = include.attributes.values("identifier").asScala
+ val source = include.source match {
+ case direct: DirectiveNode.Source.Direct => direct.value
+ case other => throw IncludeDirective.IncludeSourceException(other)
+ }
+ val includeFile = SourceDirective.resolveFile("include", source, file, properties)
+ val frontin = Frontin(includeFile)
+ val filterLabels = include.attributes.booleanValue(
+ "filterLabels",
+ properties.get("include.filterLabels").exists(_ == "true"))
+ val (text, snippetLang) = Snippet(includeFile, labels, filterLabels)
+ // I guess we could support multiple markup languages in future...
+ if (snippetLang != "md" && snippetLang != "markdown") {
+ throw IncludeDirective.IncludeFormatException(snippetLang)
+ }
+ val includedRoot = parseAndProcessMarkdown(includeFile, text, properties ++ frontin.header)
+ val includeNode = IncludeNode(includedRoot, includeFile, source)
+ includeNode.setStartIndex(include.getStartIndex)
+ includeNode.setEndIndex(include.getEndIndex)
+ newChildren.add(includeNode)
+
+ case other => newChildren.add(other)
+ }
+
+ newRoot.setReferences(root.getReferences)
+ newRoot.setAbbreviations(root.getAbbreviations)
+
+ newRoot
+ }
+
/**
* Normalize path to '/' separator.
*/
diff --git a/core/src/main/scala/com/lightbend/paradox/markdown/Directive.scala b/core/src/main/scala/com/lightbend/paradox/markdown/Directive.scala
index 44caaef2..6c76a1c3 100644
--- a/core/src/main/scala/com/lightbend/paradox/markdown/Directive.scala
+++ b/core/src/main/scala/com/lightbend/paradox/markdown/Directive.scala
@@ -20,7 +20,6 @@ import com.lightbend.paradox.tree.Tree.Location
import java.io.{ File, FileNotFoundException }
import java.util.Optional
-import com.lightbend.paradox.tree.Tree
import org.pegdown.ast._
import org.pegdown.ast.DirectiveNode.Format._
import org.pegdown.plugins.ToHtmlSerializerPlugin
@@ -99,18 +98,7 @@ trait SourceDirective { this: Directive =>
}
protected def resolveFile(propPrefix: String, source: String, page: Page, variables: Map[String, String]): File =
- source match {
- case s if s startsWith "$" =>
- val baseKey = s.drop(1).takeWhile(_ != '$')
- val base = new File(PropertyUrl(s"$propPrefix.$baseKey.base_dir", variables.get).base.trim)
- val effectiveBase = if (base.isAbsolute) base else new File(page.file.getParentFile, base.toString)
- new File(effectiveBase, s.drop(baseKey.length + 2))
- case s if s startsWith "/" =>
- val base = new File(PropertyUrl(SnipDirective.buildBaseDir, variables.get).base.trim)
- new File(base, s)
- case s =>
- new File(page.file.getParentFile, s)
- }
+ SourceDirective.resolveFile(propPrefix, source, page.file, variables)
private lazy val referenceMap: Map[String, ReferenceNode] = {
val tempRoot = new RootNode
@@ -124,6 +112,22 @@ trait SourceDirective { this: Directive =>
}
}
+object SourceDirective {
+ def resolveFile(propPrefix: String, source: String, pageFile: File, variables: Map[String, String]): File =
+ source match {
+ case s if s startsWith "$" =>
+ val baseKey = s.drop(1).takeWhile(_ != '$')
+ val base = new File(PropertyUrl(s"$propPrefix.$baseKey.base_dir", variables.get).base.trim)
+ val effectiveBase = if (base.isAbsolute) base else new File(pageFile.getParentFile, base.toString)
+ new File(effectiveBase, s.drop(baseKey.length + 2))
+ case s if s startsWith "/" =>
+ val base = new File(PropertyUrl(SnipDirective.buildBaseDir, variables.get).base.trim)
+ new File(base, s)
+ case s =>
+ new File(pageFile.getParentFile, s)
+ }
+}
+
// Default directives
/**
@@ -360,8 +364,9 @@ case class SnipDirective(page: Page, variables: Map[String, String])
try {
val labels = node.attributes.values("identifier").asScala
val source = resolvedSource(node, page)
+ val filterLabels = node.attributes.booleanValue("filterLabels", variables.get("snip.filterLabels").forall(_ == "true"))
val file = resolveFile("snip", source, page, variables)
- val (text, snippetLang) = Snippet(file, labels)
+ val (text, snippetLang) = Snippet(file, labels, filterLabels)
val lang = Option(node.attributes.value("type")).getOrElse(snippetLang)
val group = Option(node.attributes.value("group")).getOrElse("")
val sourceUrl = if (variables.contains(GitHubResolver.baseUrl) && variables.getOrElse(SnipDirective.showGithubLinks, "false") == "true") {
@@ -412,7 +417,8 @@ case class FiddleDirective(page: Page, variables: Map[String, String])
val source = resolvedSource(node, page)
val file = resolveFile("fiddle", source, page, variables)
- val (code, _) = Snippet(file, labels)
+ val filterLabels = node.attributes.booleanValue("filterLabels", variables.get("fiddle.filterLabels").forall(_ == "true"))
+ val (code, _) = Snippet(file, labels, filterLabels)
printer.println.print(s"""
@@ -444,7 +450,7 @@ object FiddleDirective {
* Placeholder to insert a serialized table of contents, using the page and header trees.
* Depth and whether to include pages or headers can be specified in directive attributes.
*/
-case class TocDirective(location: Location[Page]) extends LeafBlockDirective("toc") {
+case class TocDirective(location: Location[Page], includeIndexes: List[Int]) extends LeafBlockDirective("toc") {
def render(node: DirectiveNode, visitor: Visitor, printer: Printer): Unit = {
val classes = node.attributes.classesString
val depth = node.attributes.intValue("depth", 6)
@@ -453,7 +459,7 @@ case class TocDirective(location: Location[Page]) extends LeafBlockDirective("to
val ordered = node.attributes.booleanValue("ordered", false)
val toc = new TableOfContents(pages, headers, ordered, depth)
printer.println.print(s"""
""")
- toc.markdown(location, node.getStartIndex).accept(visitor)
+ toc.markdown(location, node.getStartIndex, includeIndexes).accept(visitor)
printer.println.print("
")
}
}
@@ -697,29 +703,14 @@ object DependencyDirective {
case class UndefinedVariable(name: String) extends RuntimeException(s"'$name' is not defined")
}
-case class IncludeDirective(context: Writer.Context) extends LeafBlockDirective("include") with SourceDirective {
-
- override def page: Page = context.location.tree.label
- override def variables = context.properties
+case class IncludeDirective(page: Page, variables: Map[String, String]) extends LeafBlockDirective("include") with SourceDirective {
override def render(node: DirectiveNode, visitor: Visitor, printer: Printer): Unit = {
- val labels = node.attributes.values("identifier").asScala
- val source = resolvedSource(node, page)
- val file = resolveFile("include", source, page, context.properties)
- val (text, snippetLang) = Snippet(file, labels)
- // I guess we could support multiple markup languages in future...
- if (snippetLang != "md" && snippetLang != "markdown") {
- throw IncludeDirective.IncludeFormatException(snippetLang)
- }
- val includeNode = context.reader.read(text)
-
- // This location has no forest around it... which probably means that things like toc and navigation can't
- // be rendered inside snippets, which I'm ok with.
- val newLocation = Location(Tree.leaf(Page.included(file, source, page, includeNode)), Nil, Nil, Nil)
- printer.print(context.writer.writeContent(includeNode, context.copy(location = newLocation)))
+ throw new IllegalStateException("Include directive should have been handled in markdown preprocessing before render, but wasn't.")
}
}
object IncludeDirective {
+ case class IncludeSourceException(source: DirectiveNode.Source) extends RuntimeException(s"Only explicit links are supported by the include directive, reference links are not: " + source)
case class IncludeFormatException(format: String) extends RuntimeException(s"Don't know how to include '*.$format' content.")
}
diff --git a/core/src/main/scala/com/lightbend/paradox/markdown/IncludeNode.scala b/core/src/main/scala/com/lightbend/paradox/markdown/IncludeNode.scala
new file mode 100644
index 00000000..3cff608f
--- /dev/null
+++ b/core/src/main/scala/com/lightbend/paradox/markdown/IncludeNode.scala
@@ -0,0 +1,47 @@
+/*
+ * 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 java.io.File
+import java.util
+
+import com.lightbend.paradox.markdown.Writer.Context
+import com.lightbend.paradox.tree.Tree
+import com.lightbend.paradox.tree.Tree.Location
+import org.pegdown.Printer
+import org.pegdown.ast.{ AbstractNode, Node, RootNode, Visitor }
+import org.pegdown.plugins.ToHtmlSerializerPlugin
+
+case class IncludeNode(included: RootNode, includedFrom: File, includedFromPath: String) extends AbstractNode {
+ override def accept(visitor: Visitor): Unit = visitor.visit(this)
+ override def getChildren: util.List[Node] = included.getChildren
+}
+
+class IncludeNodeSerializer(context: Context) extends ToHtmlSerializerPlugin {
+ override def visit(node: Node, visitor: Visitor, printer: Printer): Boolean = node match {
+ case include @ IncludeNode(included, includedFrom, includedFromPath) =>
+ // This location has no forest around it... which probably means that things like toc and navigation can't
+ // be rendered inside snippets, which I'm ok with.
+ val newLocation = Location(Tree.leaf(Page.included(includedFrom, includedFromPath,
+ context.location.tree.label, included)), context.location.lefts, context.location.rights, context.location.parents)
+ printer.print(context.writer.writeContent(included, context.copy(
+ location = newLocation,
+ includeIndexes = context.includeIndexes :+ include.getStartIndex)))
+ true
+ case _ => false
+ }
+}
\ No newline at end of file
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 76c2ebb3..c83fa775 100644
--- a/core/src/main/scala/com/lightbend/paradox/markdown/Index.scala
+++ b/core/src/main/scala/com/lightbend/paradox/markdown/Index.scala
@@ -28,7 +28,15 @@ import scala.collection.JavaConverters._
*/
object Index {
- case class Ref(level: Int, path: String, markdown: Node, group: Option[String])
+ /**
+ * @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])
@@ -46,25 +54,27 @@ object Index {
* Create a tree of header refs from a parsed markdown page.
*/
def headers(root: RootNode): Forest[Ref] = {
- Tree.hierarchy(headerRefs(root, group = None))(Ordering[Int].on[Ref](_.level))
+ 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]): List[Ref] = {
+ 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))
- case anchor: AnchorLinkNode => List(Ref(header.getLevel, "#" + anchor.getName, new TextNode(anchor.getText), group))
+ 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)
+ headerRefs(node.contentsNode.asInstanceOf[RootNode], newGroup, includeIndexes)
+ case node @ IncludeNode(included, _, _) =>
+ headerRefs(included, group, includeIndexes :+ node.getStartIndex)
case _ => Nil
}
}
@@ -109,7 +119,7 @@ object Index {
@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))
+ 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)
diff --git a/core/src/main/scala/com/lightbend/paradox/markdown/Page.scala b/core/src/main/scala/com/lightbend/paradox/markdown/Page.scala
index 5dff5466..2c4f5f01 100644
--- a/core/src/main/scala/com/lightbend/paradox/markdown/Page.scala
+++ b/core/src/main/scala/com/lightbend/paradox/markdown/Page.scala
@@ -36,7 +36,7 @@ sealed abstract class Linkable {
/**
* Header in a page, with anchor path and markdown nodes.
*/
-case class Header(path: String, label: Node, group: Option[String]) extends Linkable
+case class Header(path: String, label: Node, group: Option[String], includeIndexes: List[Int]) extends Linkable
/**
* Markdown page with target path, parsed markdown, and headers.
@@ -94,10 +94,10 @@ object Page {
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.children ++ hs)
- case Nil => (Header(targetPath, new SpecialTextNode(targetPath), None), Nil)
+ 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)))
+ 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)
}
@@ -149,8 +149,8 @@ object Page {
*/
def included(file: File, includeFilePath: String, includedIn: Page, markdown: RootNode): Page = {
val rootSrcPage = Path.relativeRootPath(file, includeFilePath)
- val h1 = Header(includedIn.path, new SpecialTextNode(includedIn.path), None)
- Page(file, includedIn.path, rootSrcPage, h1.label, h1, Nil, markdown, h1.group, includedIn.properties)
+ Page(file, includedIn.path, rootSrcPage, includedIn.h1.label, includedIn.h1, includedIn.headers, markdown,
+ includedIn.group, includedIn.properties)
}
}
diff --git a/core/src/main/scala/com/lightbend/paradox/markdown/Snippet.scala b/core/src/main/scala/com/lightbend/paradox/markdown/Snippet.scala
index 92f50e1c..34301f15 100644
--- a/core/src/main/scala/com/lightbend/paradox/markdown/Snippet.scala
+++ b/core/src/main/scala/com/lightbend/paradox/markdown/Snippet.scala
@@ -25,24 +25,26 @@ object Snippet {
class SnippetException(message: String) extends RuntimeException(message)
- def apply(file: File, labels: Seq[String]): (String, String) = {
+ def apply(file: File, labels: Seq[String], filterLabelLines: Boolean): (String, String) = {
val source = Source.fromFile(file)("UTF-8")
try {
val lines = source.getLines.toSeq
- (extract(file, lines, labels), language(file))
+ (extract(file, lines, labels, filterLabelLines), language(file))
} finally {
source.close()
}
}
- def extract(file: File, lines: Seq[String], labels: Seq[String]): String = {
+ def extract(file: File, lines: Seq[String], labels: Seq[String], filterLabelLines: Boolean): String = {
labels match {
case Seq() =>
- val extractionState = extractFrom(lines, _ => true, _ => false, addFilteredLine)
- cutIndentation(extractionState.snippetLines)
+ val addLine = addFilteredLine(filterLabelLines)
+ cutIndentation(lines.zipWithIndex.foldLeft(Seq.empty[(Int, String)]) {
+ case (lines, (line, lineIndex)) => addLine(line, lines, lineIndex + 1)
+ }.map(_._2))
case _ =>
labels.map { label =>
- val extractionState = extractState(file, lines, label)
+ val extractionState = extractState(file, lines, label, filterLabelLines)
cutIndentation(extractionState.snippetLines)
}.mkString("\n")
}
@@ -62,11 +64,11 @@ object Snippet {
snippetLines.map(ln => dropIndent(minIndent, ln)).mkString("\n")
}
- def extractLabelRange(file: File, label: String): Option[(Int, Int)] = {
+ def extractLabelRange(file: File, label: String, filterLabelLines: Boolean = true): Option[(Int, Int)] = {
val source = Source.fromFile(file)("UTF-8")
try {
val lines = source.getLines.toSeq
- val lineNumbers = extractState(file, lines, label).lines.map(_._1)
+ val lineNumbers = extractState(file, lines, label, filterLabelLines).lines.map(_._1)
if (lineNumbers.isEmpty)
None
else
@@ -78,7 +80,7 @@ object Snippet {
type Line = (Int, String)
- private def extractState(file: File, lines: Seq[String], label: String): ExtractionState = {
+ private def extractState(file: File, lines: Seq[String], label: String, filterLabelLines: Boolean): ExtractionState = {
if (!verifyLabel(label)) throw new SnippetException(s"Label [$label] for [$file] contains illegal characters. " +
"Only [a-zA-Z0-9_-] are allowed.")
// A label can be followed by an end of line or one or more spaces followed by an
@@ -86,7 +88,7 @@ object Snippet {
// (anything not in the group [a-zA-Z0-9_])
val labelPattern = ("""#\Q""" + label + """\E( +[^w \t]*)?$""").r
val hasLabel = (s: String) => labelPattern.findFirstIn(s).nonEmpty
- val extractionState = extractFrom(lines, hasLabel, hasLabel, addFilteredLine)
+ val extractionState = extractFrom(lines, hasLabel, hasLabel, addFilteredLine(filterLabelLines))
if (extractionState.snippetLines.isEmpty)
throw new SnippetException(s"Label [$label] not found in [$file]")
extractionState.block match {
@@ -100,11 +102,9 @@ object Snippet {
lines.zipWithIndex.foldLeft(ExtractionState(block = NoBlock, lines = Seq.empty)) {
case (es, (l, lineIndex)) =>
es.block match {
- case NoBlock if blockStart(l) =>
- es.copy(block = InBlock, lines = addLine(l, es.lines, lineIndex + 1))
- case NoBlock => es
- case InBlock if blockEnd(l) =>
- es.copy(block = NoBlock, lines = addLine(l, es.lines, lineIndex + 1))
+ case NoBlock if blockStart(l) => es.withBlock(InBlock)
+ case NoBlock => es
+ case InBlock if blockEnd(l) => es.withBlock(NoBlock)
case InBlock =>
es.copy(lines = addLine(l, es.lines, lineIndex + 1))
}
@@ -126,6 +126,7 @@ object Snippet {
private case class ExtractionState(block: Block, lines: Seq[Line]) {
def snippetLines = lines.map(_._2)
+ def withBlock(block: Block) = copy(block = block)
}
private sealed trait Block
@@ -137,9 +138,15 @@ object Snippet {
private def containsLabel(line: String): Option[String] =
anyLabelRegex.findFirstIn(line)
- private def addFilteredLine(line: String, lines: Seq[Line], lineNumber: Int): Seq[Line] =
+ private def addFilteredLine(filterLabels: Boolean): (String, Seq[Line], Int) => Seq[Line] =
+ if (filterLabels) addLineFilterLabels else addLineNonFiltered
+
+ private def addLineFilterLabels(line: String, lines: Seq[Line], lineNumber: Int): Seq[Line] =
containsLabel(line).map(_ => lines).getOrElse(lines :+ (lineNumber, line))
+ private def addLineNonFiltered(line: String, lines: Seq[Line], lineNumber: Int): Seq[Line] =
+ lines :+ (lineNumber, line)
+
private def verifyLabel(label: String): Boolean = containsLabel(s"#$label").nonEmpty
def language(file: File): String = {
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 f0f2685d..0215442f 100644
--- a/core/src/main/scala/com/lightbend/paradox/markdown/TableOfContents.scala
+++ b/core/src/main/scala/com/lightbend/paradox/markdown/TableOfContents.scala
@@ -35,8 +35,14 @@ class TableOfContents(pages: Boolean = true, headers: Boolean = true, ordered: B
/**
* 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, Some(location), nested(location.tree, tocIndex))
+ @deprecated("0.5.1", "Use the includeIndexes variant to ensure it works with include files")
+ def markdown(location: Location[Page], tocIndex: Int): Node = markdown(location, tocIndex, Nil)
+
+ /**
+ * Create a TOC bullet list for a TOC at a certain point within the section hierarchy.
+ */
+ def markdown(location: Location[Page], tocIndex: Int, includeIndexes: List[Int]): Node = {
+ markdown(location.tree.label.base, Some(location), nested(location.tree, tocIndex, includeIndexes))
}
/**
@@ -65,9 +71,9 @@ class TableOfContents(pages: Boolean = true, headers: Boolean = true, ordered: B
/**
* Create a new Page Tree for a TOC at a certain point within the section hierarchy.
*/
- private def nested(tree: Tree[Page], tocIndex: Int): Tree[Page] = {
+ private def nested(tree: Tree[Page], tocIndex: Int, includeIndexes: List[Int]): Tree[Page] = {
val page = tree.label
- val (level, headers) = headersBelow(Location.forest(page.headers), tocIndex)
+ val (level, headers) = headersBelow(Location.forest(page.headers), tocIndex, includeIndexes)
val subPages = if (level == 0) tree.children else Nil
Tree(page.copy(headers = headers), subPages)
}
@@ -76,13 +82,24 @@ class TableOfContents(pages: Boolean = true, headers: Boolean = true, ordered: B
* Find the headers below the buffer index for a toc directive.
* Return the level of the next header and sub-headers to render.
*/
- private def headersBelow(location: Option[Location[Header]], index: Int): (Int, Forest[Header]) = location match {
+ private def headersBelow(location: Option[Location[Header]], index: Int, includeIndexes: List[Int]): (Int, Forest[Header]) = location match {
case Some(loc) =>
- if (loc.tree.label.label.getStartIndex > index) (loc.depth, loc.tree :: loc.rights)
- else headersBelow(loc.next, index)
+ if (isBelow(index, includeIndexes, loc.tree.label.label.getStartIndex, loc.tree.label.includeIndexes))
+ (loc.depth, loc.tree :: loc.rights)
+ else headersBelow(loc.next, index, includeIndexes)
case None => (0, Nil)
}
+ private def isBelow(tocIndex: Int, tocIncludeIndexes: List[Int], headerIndex: Int, headerIncludeIndexes: List[Int]): Boolean = {
+ // If the current level of include indexes are equal, then we need to recursively check the next level.
+ // Otherwise, we compare the current level of include indexes if they exist, or the current indexes themselves.
+ (tocIncludeIndexes, headerIncludeIndexes) match {
+ case (i :: itail, h :: htail) if i == h => isBelow(tocIndex, itail, headerIndex, htail)
+ case _ =>
+ headerIncludeIndexes.headOption.getOrElse(headerIndex) > tocIncludeIndexes.headOption.getOrElse(tocIndex)
+ }
+ }
+
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 =>
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 8dae51e3..2a30cc24 100644
--- a/core/src/main/scala/com/lightbend/paradox/markdown/Writer.scala
+++ b/core/src/main/scala/com/lightbend/paradox/markdown/Writer.scala
@@ -94,15 +94,17 @@ object Writer {
* Write context which is passed through to directives.
*/
case class Context(
- location: Location[Page],
- paths: Set[String],
- reader: Reader,
- writer: Writer,
- pageMappings: String => String = Path.replaceExtension(DefaultSourceSuffix, DefaultTargetSuffix),
- sourceSuffix: String = DefaultSourceSuffix,
- targetSuffix: String = DefaultTargetSuffix,
- groups: Map[String, Seq[String]] = Map.empty,
- properties: Map[String, String] = Map.empty)
+ location: Location[Page],
+ paths: Set[String],
+ reader: Reader,
+ writer: Writer,
+ pageMappings: String => String = Path.replaceExtension(DefaultSourceSuffix, DefaultTargetSuffix),
+ sourceSuffix: String = DefaultSourceSuffix,
+ targetSuffix: String = DefaultTargetSuffix,
+ groups: Map[String, Seq[String]] = Map.empty,
+ properties: Map[String, String] = Map.empty,
+ includeIndexes: List[Int] = Nil
+ )
def defaultLinks(context: Context): LinkRenderer =
new DefaultLinkRenderer(context)
@@ -117,7 +119,8 @@ object Writer {
def defaultPlugins(directives: Seq[Context => Directive]): Seq[Context => ToHtmlSerializerPlugin] = Seq(
context => new ClassyLinkSerializer,
context => new AnchorLinkSerializer,
- context => new DirectiveSerializer(directives.map(d => d(context)))
+ context => new DirectiveSerializer(directives.map(d => d(context))),
+ context => new IncludeNodeSerializer(context)
)
def defaultDirectives: Seq[Context => Directive] = Seq(
@@ -128,7 +131,7 @@ object Writer {
context => GitHubDirective(context.location.tree.label, context.properties),
context => SnipDirective(context.location.tree.label, context.properties),
context => FiddleDirective(context.location.tree.label, context.properties),
- context => TocDirective(context.location),
+ context => TocDirective(context.location, context.includeIndexes),
context => VarDirective(context.properties),
context => VarsDirective(context.properties),
context => CalloutDirective("note", "Note"),
@@ -137,7 +140,7 @@ object Writer {
context => InlineWrapDirective("span"),
context => InlineGroupDirective(context.groups.values.flatten.map(_.toLowerCase).toSeq),
context => DependencyDirective(context.properties),
- context => IncludeDirective(context)
+ context => IncludeDirective(context.location.tree.label, context.properties)
)
class DefaultLinkRenderer(context: Context) extends LinkRenderer {
diff --git a/testkit/src/main/scala/com/lightbend/paradox/markdown/MarkdownTestkit.scala b/testkit/src/main/scala/com/lightbend/paradox/markdown/MarkdownTestkit.scala
index a2f8101b..eef1f409 100644
--- a/testkit/src/main/scala/com/lightbend/paradox/markdown/MarkdownTestkit.scala
+++ b/testkit/src/main/scala/com/lightbend/paradox/markdown/MarkdownTestkit.scala
@@ -18,13 +18,17 @@ package com.lightbend.paradox.markdown
import com.lightbend.paradox.tree.Tree.{ Forest, Location }
import java.io.{ File, PrintWriter }
+
import com.lightbend.paradox.template.PageTemplate
import java.nio.file._
+import com.lightbend.paradox.ParadoxProcessor
+
abstract class MarkdownTestkit {
val markdownReader = new Reader
val markdownWriter = new Writer
+ val paradoxProcessor = new ParadoxProcessor(markdownReader, markdownWriter)
def markdown(text: String)(implicit context: Location[Page] => Writer.Context = writerContext): String = {
markdownPages("test.md" -> text).getOrElse("test.html", "")
@@ -96,7 +100,8 @@ abstract class MarkdownTestkit {
val parsed = mappings map {
case (path, text) =>
val frontin = Frontin(prepare(text))
- (new File(path), path, markdownReader.read(frontin.body), frontin.header)
+ val file = new File(path)
+ (new File(path), path, paradoxProcessor.parseAndProcessMarkdown(file, frontin.body, globalProperties ++ frontin.header), frontin.header)
}
Page.forest(parsed, Path.replaceSuffix(Writer.DefaultSourceSuffix, Writer.DefaultTargetSuffix), globalProperties)
}
@@ -123,7 +128,7 @@ abstract class MarkdownTestkit {
tidy.setShowWarnings(false)
tidy.setQuiet(true)
tidy.parse(reader, writer)
- writer.toString.replace("\r\n", "\n").replace("\r", "\n")
+ writer.toString.replace("\r\n", "\n").replace("\r", "\n").trim
}
case class PartialPageContent(properties: Map[String, String], content: String) extends PageTemplate.Contents {
diff --git a/tests/src/test/resources/example.conf b/tests/src/test/resources/example.conf
new file mode 100644
index 00000000..979703b2
--- /dev/null
+++ b/tests/src/test/resources/example.conf
@@ -0,0 +1,17 @@
+# 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.
+
+//#example
+a = b
+//#example
diff --git a/tests/src/test/resources/headers.md b/tests/src/test/resources/headers.md
new file mode 100644
index 00000000..477c2131
--- /dev/null
+++ b/tests/src/test/resources/headers.md
@@ -0,0 +1,2 @@
+## Heading 1
+## Heading 2
\ No newline at end of file
diff --git a/tests/src/test/resources/include-code-snip.md b/tests/src/test/resources/include-code-snip.md
new file mode 100644
index 00000000..b7a03808
--- /dev/null
+++ b/tests/src/test/resources/include-code-snip.md
@@ -0,0 +1 @@
+@@snip[example.conf](example.conf) { #example }
diff --git a/tests/src/test/resources/toc.md b/tests/src/test/resources/toc.md
new file mode 100644
index 00000000..6aeb98b5
--- /dev/null
+++ b/tests/src/test/resources/toc.md
@@ -0,0 +1,9 @@
+
+## This shouldn't be included since it's not below the toc
+
+More text here to ensure that the headers in the outer file which are below this pages
+inclusion but with a higher start index do still get included.
+
+Blah blah blah blah blah blah blah.
+
+@@toc { depth=1 }
diff --git a/tests/src/test/scala/com/lightbend/paradox/markdown/IncludeDirectiveSpec.scala b/tests/src/test/scala/com/lightbend/paradox/markdown/IncludeDirectiveSpec.scala
index 09353221..7ec85948 100644
--- a/tests/src/test/scala/com/lightbend/paradox/markdown/IncludeDirectiveSpec.scala
+++ b/tests/src/test/scala/com/lightbend/paradox/markdown/IncludeDirectiveSpec.scala
@@ -50,4 +50,59 @@ class IncludeDirectiveSpec extends MarkdownBaseSpec {
|This file should be included by IncludeDirectiveSpec
""")
}
+ it should "include nested code snippets" in {
+ markdown("""@@include(tests/src/test/resources/include-code-snip.md)""") shouldEqual html("""
+ |
+ |
+ |a = b
+ |
""")
+ }
+
+ it should "include headers from nested snippets in the toc" in {
+ markdown(
+ """
+ |# Page heading
+ |
+ |This text appears here to push down the toc so to ensure that the headers below
+ |calculation works.
+ |
+ |@@toc { depth=1 }
+ |
+ |@@include(tests/src/test/resources/headers.md)
+ |
+ |## Heading 3
+ |
+ |""") should include(html("""
+ |"""))
+ }
+
+ it should "include headers from outer snippets in a nested toc" in {
+ markdown(
+ """
+ |# Page heading
+ |
+ |## Above toc
+ |
+ |@@include(tests/src/test/resources/toc.md)
+ |
+ |## Heading 1
+ |## Heading 2
+ |## Heading 3
+ |
+ |""") should include(html("""
+ |"""))
+ }
+
}
diff --git a/tests/src/test/scala/com/lightbend/paradox/markdown/SnipDirectiveSpec.scala b/tests/src/test/scala/com/lightbend/paradox/markdown/SnipDirectiveSpec.scala
index 0c01649d..473b4659 100644
--- a/tests/src/test/scala/com/lightbend/paradox/markdown/SnipDirectiveSpec.scala
+++ b/tests/src/test/scala/com/lightbend/paradox/markdown/SnipDirectiveSpec.scala
@@ -153,4 +153,25 @@ class SnipDirectiveSpec extends MarkdownBaseSpec {
|}
|""")
}
+
+ it should "filter labels by default" in {
+ markdown("""@@snip[example.scala](tests/src/test/scala/com/lightbend/paradox/markdown/example.scala) { #example-with-label }""") shouldEqual html(
+ """
+ |
+ |object Constants {
+ |}
+ |
"""
+ )
+ }
+
+ it should "allow including labels if specified" in {
+ markdown("""@@snip[example.scala](tests/src/test/scala/com/lightbend/paradox/markdown/example.scala) { #example-with-label filterLabels=false }""") shouldEqual html(
+ """
+ |
+ |object Constants {
+ | val someString = " #foo "
+ |}
+ |
"""
+ )
+ }
}
diff --git a/tests/src/test/scala/com/lightbend/paradox/markdown/SnippetIndentationTest.scala b/tests/src/test/scala/com/lightbend/paradox/markdown/SnippetIndentationTest.scala
index df7039ef..fd770b34 100644
--- a/tests/src/test/scala/com/lightbend/paradox/markdown/SnippetIndentationTest.scala
+++ b/tests/src/test/scala/com/lightbend/paradox/markdown/SnippetIndentationTest.scala
@@ -151,6 +151,6 @@ class SnippetIndentationTest extends FlatSpec with Matchers {
def extractToString(inString: String, label: String, indentationPerSnippet: Boolean = true): String = {
val in = inString.split("\n").toList
- Snippet.extract(new File(""), in, Seq(label))
+ Snippet.extract(new File(""), in, Seq(label), true)
}
}
diff --git a/tests/src/test/scala/com/lightbend/paradox/markdown/example.scala b/tests/src/test/scala/com/lightbend/paradox/markdown/example.scala
index 690a061b..0116ddb6 100644
--- a/tests/src/test/scala/com/lightbend/paradox/markdown/example.scala
+++ b/tests/src/test/scala/com/lightbend/paradox/markdown/example.scala
@@ -73,3 +73,10 @@ class AnotherClass
//#multi-indented-example
// format: ON
+
+
+//#example-with-label
+object Constants {
+ val someString = " #foo "
+}
+//#example-with-label
\ No newline at end of file