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