diff --git a/build.sbt b/build.sbt index 80912a1c..6d3d1428 100644 --- a/build.sbt +++ b/build.sbt @@ -107,6 +107,7 @@ lazy val docs = (project in file("docs")) name := "paradox docs", paradoxTheme := Some(builtinParadoxTheme("generic")), paradoxProperties in Compile += ("empty" -> "") + //paradoxGroups := Map("Languages" -> Seq("Scala", "Java")) ) addCommandAlias("verify", ";test ;scripted ;docs/paradox") diff --git a/core/src/main/java/org/pegdown/ast/VerbatimGroupNode.java b/core/src/main/java/org/pegdown/ast/VerbatimGroupNode.java new file mode 100644 index 00000000..c6c49791 --- /dev/null +++ b/core/src/main/java/org/pegdown/ast/VerbatimGroupNode.java @@ -0,0 +1,43 @@ +/* + * 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 org.pegdown.ast; + +public class VerbatimGroupNode extends VerbatimNode { + private final String group; + + public VerbatimGroupNode(String text) { + this(text, "", ""); + } + + public VerbatimGroupNode(String text, String type) { + this(text, type, ""); + } + + public VerbatimGroupNode(String text, String type, String group) { + super(text, type); + this.group = group; + } + + @Override + public void accept(Visitor visitor) { + visitor.visit(this); + } + + public String getGroup() { + return group; + } +} \ No newline at end of file diff --git a/core/src/main/scala/com/lightbend/paradox/ParadoxProcessor.scala b/core/src/main/scala/com/lightbend/paradox/ParadoxProcessor.scala index 5a5cf1e4..64307e81 100644 --- a/core/src/main/scala/com/lightbend/paradox/ParadoxProcessor.scala +++ b/core/src/main/scala/com/lightbend/paradox/ParadoxProcessor.scala @@ -16,8 +16,8 @@ package com.lightbend.paradox -import com.lightbend.paradox.markdown.{ Breadcrumbs, Page, Path, Reader, TableOfContents, Writer, Frontin, PropertyUrl, Url } 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 } @@ -38,10 +38,13 @@ class ParadoxProcessor(reader: Reader = new Reader, writer: Writer = new Writer) outputDirectory: File, sourceSuffix: String, targetSuffix: String, + groups: Map[String, Seq[String]], properties: Map[String, String], navigationDepth: Int, pageTemplate: PageTemplate, errorListener: STErrorListener): Seq[(File, String)] = { + require(!groups.values.flatten.map(_.toLowerCase).groupBy(identity).values.exists(_.size > 1), "Group names may not overlap") + val pages = parsePages(mappings, Path.replaceSuffix(sourceSuffix, targetSuffix)) val paths = Page.allPaths(pages).toSet val globalPageMappings = rootPageMappings(pages) @@ -52,10 +55,10 @@ class ParadoxProcessor(reader: Reader = new Reader, writer: Writer = new Writer) val page = loc.tree.label val pageProperties = properties ++ page.properties.get val currentMapping = Path.generateTargetFile(Path.relativeLocalPath(page.rootSrcPage, page.file.getPath), globalPageMappings)_ - val writerContext = Writer.Context(loc, paths, currentMapping, sourceSuffix, targetSuffix, pageProperties) + 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, loc, writer, writerContext, pageToc, headerToc) + val pageContext = PageContents(leadingBreadcrumbs, groups, loc, writer, writerContext, pageToc, headerToc) val outputFile = new File(outputDirectory, page.path) outputFile.getParentFile.mkdirs pageTemplate.write(page.properties(Page.Properties.DefaultLayoutMdIndicator, pageTemplate.defaultName), pageContext, outputFile, errorListener) @@ -68,7 +71,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)], 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, pageToc: TableOfContents, headerToc: TableOfContents) extends PageTemplate.Contents { import scala.collection.JavaConverters._ private val page = loc.tree.label @@ -83,6 +86,7 @@ class ParadoxProcessor(reader: Reader = new Reader, writer: Writer = new Writer) 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 getGroups = Groups.html(groups) lazy val hasSubheaders = page.headers.nonEmpty lazy val getToc = writer.writeToc(headerToc.headers(loc), context) lazy val getSource_url = githubLink(Some(loc)).getHtml 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 520f7902..8d42001d 100644 --- a/core/src/main/scala/com/lightbend/paradox/markdown/Directive.scala +++ b/core/src/main/scala/com/lightbend/paradox/markdown/Directive.scala @@ -311,7 +311,8 @@ case class SnipDirective(page: Page, variables: Map[String, String]) } else new File(page.file.getParentFile, source) val text = Snippet(file, labels) val lang = Option(node.attributes.value("type")).getOrElse(Snippet.language(file)) - new VerbatimNode(text, lang).accept(visitor) + val group = Option(node.attributes.value("group")).getOrElse("") + new VerbatimGroupNode(text, lang, group).accept(visitor) } catch { case e: FileNotFoundException => throw new SnipDirective.LinkException(s"Unknown snippet [${e.getMessage}] referenced from [${page.path}]") @@ -473,3 +474,34 @@ case class WrapDirective(typ: String) extends ContainerBlockDirective(Array(typ, printer.print(s"") } } + +/** + * Inline wrap directive + * + * Wraps inner contents in a `span`, optionally with custom `id` and/or `class` attributes. + */ +case class InlineWrapDirective(typ: String) extends InlineDirective("span") { + def render(node: DirectiveNode, visitor: Visitor, printer: Printer): Unit = { + val id = + node.attributes.identifier match { + case null => "" + case x => s""" id="$x"""" + } + val classes = + node.attributes.classesString match { + case "" => "" + case x => s""" class="$x"""" + } + printer.print(s"""<$typ$id$classes>""") + node.contentsNode.accept(visitor) + printer.print(s"") + } +} + +case class InlineGroupDirective(groups: Seq[String]) extends InlineDirective(groups: _*) { + def render(node: DirectiveNode, visitor: Visitor, printer: Printer): Unit = { + printer.print(s"""""") + node.contentsNode.accept(visitor) + printer.print(s"") + } +} diff --git a/core/src/main/scala/com/lightbend/paradox/markdown/Groups.scala b/core/src/main/scala/com/lightbend/paradox/markdown/Groups.scala new file mode 100644 index 00000000..2a35feab --- /dev/null +++ b/core/src/main/scala/com/lightbend/paradox/markdown/Groups.scala @@ -0,0 +1,28 @@ +/* + * 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 + +object Groups { + def html(supergroups: Map[String, Seq[String]]) = { + supergroups.map { + case (supergroup, groups) => + s"""" + }.mkString("\n") + } +} \ No newline at end of file diff --git a/core/src/main/scala/com/lightbend/paradox/markdown/StyledVerbatim.scala b/core/src/main/scala/com/lightbend/paradox/markdown/StyledVerbatim.scala index 094b5f0c..374c3674 100644 --- a/core/src/main/scala/com/lightbend/paradox/markdown/StyledVerbatim.scala +++ b/core/src/main/scala/com/lightbend/paradox/markdown/StyledVerbatim.scala @@ -17,7 +17,7 @@ package com.lightbend.paradox.markdown import org.parboiled.common.StringUtils -import org.pegdown.ast.VerbatimNode +import org.pegdown.ast.{ VerbatimNode, VerbatimGroupNode } import org.pegdown.{ Printer, VerbatimSerializer } /** @@ -25,7 +25,7 @@ import org.pegdown.{ Printer, VerbatimSerializer } */ abstract class StyledVerbatimSerializer extends VerbatimSerializer { - def printPreAttributes(printer: Printer): Unit + def printPreAttributes(printer: Printer, nodeGroup: String = ""): Unit def printCodeAttributes(printer: Printer, nodeType: String): Unit @@ -34,7 +34,10 @@ abstract class StyledVerbatimSerializer extends VerbatimSerializer { printer.print(" printPreAttributes(printer, vgn.getGroup) + case vn: VerbatimNode => printPreAttributes(printer) + } } printer.print(">") @@ -66,7 +69,13 @@ abstract class StyledVerbatimSerializer extends VerbatimSerializer { * Add prettify markup around verbatim blocks. */ object PrettifyVerbatimSerializer extends StyledVerbatimSerializer { - override def printPreAttributes(printer: Printer): Unit = printClass(printer, "prettyprint") + override def printPreAttributes(printer: Printer, nodeGroup: String): Unit = { + nodeGroup match { + case "" => printClass(printer, "prettyprint") + case g => printClass(printer, "prettyprint group-" + g) + } + } + override def printCodeAttributes(printer: Printer, nodeType: String): Unit = nodeType match { case "text" | "nocode" => printClass(printer, "nocode") case _ => printClass(printer, s"language-$nodeType") 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 e704d507..f1ba8b24 100644 --- a/core/src/main/scala/com/lightbend/paradox/markdown/Writer.scala +++ b/core/src/main/scala/com/lightbend/paradox/markdown/Writer.scala @@ -54,6 +54,12 @@ class Writer(serializer: Writer.Context => ToHtmlSerializer) { def writeNavigation(node: Node, context: Writer.Context): String = writeFragment(node, context) + /** + * Write groups fragment. + */ + def writeGroups(node: Node, context: Writer.Context): String = + writeFragment(node, context) + /** * Write navigation fragment. */ @@ -89,10 +95,11 @@ object Writer { case class Context( location: Location[Page], paths: Set[String], - pageMappings: String => String = Path.replaceExtension(DefaultSourceSuffix, DefaultTargetSuffix), - sourceSuffix: String = DefaultSourceSuffix, - targetSuffix: String = DefaultTargetSuffix, - properties: Map[String, String] = Map.empty) + 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) def defaultLinks(context: Context): LinkRenderer = new DefaultLinkRenderer(context) @@ -120,7 +127,10 @@ object Writer { VarsDirective(context.properties), CalloutDirective("note", "Note"), CalloutDirective("warning", "Warning"), - WrapDirective("div")) + WrapDirective("div"), + InlineWrapDirective("span"), + InlineGroupDirective(context.groups.values.flatten.map(_.toLowerCase).toSeq) + ) class DefaultLinkRenderer(context: Context) extends LinkRenderer { private lazy val imgBase = { diff --git a/core/src/test/scala/com/lightbend/paradox/markdown/InlineGroupDirectiveSpec.scala b/core/src/test/scala/com/lightbend/paradox/markdown/InlineGroupDirectiveSpec.scala new file mode 100644 index 00000000..5eb97e69 --- /dev/null +++ b/core/src/test/scala/com/lightbend/paradox/markdown/InlineGroupDirectiveSpec.scala @@ -0,0 +1,27 @@ +/* + * 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.Location + +class InlineGroupDirectiveSpec extends MarkdownBaseSpec { + + "The inline `group` directive" should "render wrapping groups" in { + markdown("@scala[Simple sentence here]") shouldEqual html("""

Simple sentence here

""") + } + +} diff --git a/core/src/test/scala/com/lightbend/paradox/markdown/InlineWrapDirectiveSpec.scala b/core/src/test/scala/com/lightbend/paradox/markdown/InlineWrapDirectiveSpec.scala new file mode 100644 index 00000000..b2a1db36 --- /dev/null +++ b/core/src/test/scala/com/lightbend/paradox/markdown/InlineWrapDirectiveSpec.scala @@ -0,0 +1,36 @@ +/* + * 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.Location + +class InlineWrapDirectiveSpec extends MarkdownBaseSpec { + + "The inline `wrap` directive" should "render wrapping `span`s" in { + markdown("@span[Simple sentence here]") shouldEqual html("

Simple sentence here

") + } + + it should "render the example from the docs" in { + markdown("@span[Scala variant containing ***markdown*** and @ref:[Linking](test.md)] { .group-scala }") shouldEqual html(""" + |

Scala variant containing markdown and Linking

""") + } + + it should "support a custom id and custom CSS classes at the same time" in { + markdown("@span[Simple sentence here.] { #yeah .red .blue }") shouldEqual html("""

Simple sentence here.

""") + } + +} diff --git a/core/src/test/scala/com/lightbend/paradox/markdown/MarkdownBaseSpec.scala b/core/src/test/scala/com/lightbend/paradox/markdown/MarkdownBaseSpec.scala index 58cc33e1..dde9f3df 100644 --- a/core/src/test/scala/com/lightbend/paradox/markdown/MarkdownBaseSpec.scala +++ b/core/src/test/scala/com/lightbend/paradox/markdown/MarkdownBaseSpec.scala @@ -81,7 +81,11 @@ abstract class MarkdownBaseSpec extends FlatSpec with Matchers { } def writerContext(location: Location[Page]): Writer.Context = { - Writer.Context(location, Page.allPaths(List(location.root.tree)).toSet) + Writer.Context( + location, + Page.allPaths(List(location.root.tree)).toSet, + groups = Map("Language" -> Seq("Scala", "Java")) + ) } def pages(mappings: (String, String)*): Forest[Page] = { diff --git a/core/src/test/scala/com/lightbend/paradox/markdown/SnipDirectiveSpec.scala b/core/src/test/scala/com/lightbend/paradox/markdown/SnipDirectiveSpec.scala new file mode 100644 index 00000000..b3d6dc4e --- /dev/null +++ b/core/src/test/scala/com/lightbend/paradox/markdown/SnipDirectiveSpec.scala @@ -0,0 +1,81 @@ +/* + * 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.Location + +class SnipDirectiveSpec extends MarkdownBaseSpec { + + val testProperties = Map("version" -> "1.2.3") + + implicit val context: Location[Page] => Writer.Context = { loc => + writerContext(loc).copy(properties = testProperties) + } + + "The `snip` directive" should "render code snippets" in { + markdown("""@@snip[example.scala](core/src/test/scala/com/lightbend/paradox/markdown/example.scala) {#example }""") shouldEqual html(""" + |
+      |
+      |object example extends App {
+      |  println("Hello, World!")
+      |}
+      |
""") + } + + it should "render code snippets in definition lists" in { + markdown(""" + |Scala + |: @@snip[example.scala](core/src/test/scala/com/lightbend/paradox/markdown/example.scala) { #example } + | + |Java + |: @@snip[example2.java](core/src/test/scala/com/lightbend/paradox/markdown/example2.java) { #example2 } + |""") shouldEqual html(""" + |
+ |
Scala
+ |
+ |
+      |
+      |object example extends App {
+      |  println("Hello, World!")
+      |}
+      |
+ |
+ |
Java
+ |
+ |
+      |
+      |public class example2 {
+      |    public static void main(String[] args) {
+      |        System.out.println("Hello, World");
+      |    }
+      |}
+      |
+ |
+ |
""") + } + + it should "support a custom id and custom CSS classes at the same time" in { + markdown(""" + |@@@ div { #yeah .red .blue } + |Simple sentence here. + |@@@""") shouldEqual html(""" + |
+ |

Simple sentence here.

+ |
""") + } + +} diff --git a/core/src/test/scala/com/lightbend/paradox/markdown/example.scala b/core/src/test/scala/com/lightbend/paradox/markdown/example.scala new file mode 100644 index 00000000..943bcc64 --- /dev/null +++ b/core/src/test/scala/com/lightbend/paradox/markdown/example.scala @@ -0,0 +1,23 @@ +/* + * 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 + +//#example +object example extends App { + println("Hello, World!") +} +//#example diff --git a/core/src/test/scala/com/lightbend/paradox/markdown/example2.java b/core/src/test/scala/com/lightbend/paradox/markdown/example2.java new file mode 100644 index 00000000..384c3cd3 --- /dev/null +++ b/core/src/test/scala/com/lightbend/paradox/markdown/example2.java @@ -0,0 +1,25 @@ +/* + * 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; + +//#example2 +public class example2 { + public static void main(String[] args) { + System.out.println("Hello, World"); + } +} +//#example2 diff --git a/docs/src/main/paradox/features/groups.md b/docs/src/main/paradox/features/groups.md new file mode 100644 index 00000000..8347e450 --- /dev/null +++ b/docs/src/main/paradox/features/groups.md @@ -0,0 +1,102 @@ +Groups +------ + +Paradox supports 'groups' which allow users to easily switch between different +'variations' of the documentation. + +## Configuration + +Groups must be configured through sbt: + +``` +.enablePlugins(NoPublish, ParadoxPlugin) + .settings( + name := "paradox docs", + paradoxTheme := Some(builtinParadoxTheme("generic")), + paradoxGroups := Map("Languages" -> Seq("Scala", "Java")) + ) +``` + +## Syntax + +### Tabs + +Groups are used for tabs, where the group is determined by the tab name: + +@@@vars +```markdown +Java +: @@snip [example-first.java](../../resources/tab-switching/examples.java) { #java_first } +$empty$ +Scala +: @@snip [example-first.scala](../../resources/tab-switching/examples.scala) { #scala_first } +``` +@@@ + +Java +: @@snip [example-first.java](../../resources/tab-switching/examples.java) { #java_first } + +Scala +: @@snip [example-first.scala](../../resources/tab-switching/examples.scala) { #scala_first } + +or by the group parameter on the snippet: + +@@@vars +```markdown +example-first.java +: @@snip [example-first.java](../../resources/tab-switching/examples.java) { #java_first group=java } +$empty$ +example-first.scala +: @@snip [example-first.scala](../../resources/tab-switching/examples.scala) { #scala_first group=scala } +``` +@@@ + +example-first.java +: @@snip [example-first.java](../../resources/tab-switching/examples.java) { #java_first group=java } + +example-first.scala +: @@snip [example-first.scala](../../resources/tab-switching/examples.scala) { #scala_first group=scala } + + +But tabs not associated with groups are left alone: + +sbt +: @@snip [build.sbt](../../resources/build.sbt) { #setup_example } + +Maven +: @@snip [pom.xml](../../resources/pom.xml) { #setup_example } + +Gradle +: Non-snippet tab body + + +### Inline + +For each group a directive is automatically created. This can be used for +switching inline text: + +``` +Text describing the @java[Java variant]@scala[Scala variant containing ***markdown*** and @ref:[Linking](linking.md)]. +``` + +Text describing the @java[Java variant]@scala[Scala variant containing ***markdown*** and @ref:[Linking](linking.md)]. + + +## Behavior + +Switching is currently done in javascript. When the page loads, a script in +`page.js` looks for entities with class `supergroup` and derives the groups +catalog from that. If you use a custom theme, use `$page.groups$` to generate +the catalog in javascript. + +The currently selected group for each category is stored in a cookie. + +## Extending + +You can register an event listener that will be called whenever a group is switched: + +``` +window.groupChanged(function(group, supergroup, catalog) { + // your code here +}); +``` \ No newline at end of file diff --git a/docs/src/main/paradox/features/snippet-inclusion.md b/docs/src/main/paradox/features/snippet-inclusion.md index 6c446b92..48995577 100644 --- a/docs/src/main/paradox/features/snippet-inclusion.md +++ b/docs/src/main/paradox/features/snippet-inclusion.md @@ -58,6 +58,52 @@ should not be highlighted set `type=text` in the directive's attribute section: @@snip [example.log](example.log) { #example-log type=text } ``` +#### tab switching + +It is possible to associate multiple snippets under the same "tag". If some tab of a snippet is switched by the user, all tabs associated with the selected one will be switched as well. + +@@@vars +```markdown +First-java +: @@snip [example-first.java](../../resources/tab-switching/examples.java) { #java_first } +$empty$ +First-scala +: @@snip [example-first.scala](../../resources/tab-switching/examples.scala) { #scala_first } +$empty$ +Some separator. +$empty$ +Java +: @@snip [example-second.java](../../resources/tab-switching/examples.java) +$empty$ +Scala +: @@snip [example-second.scala](../../resources/tab-switching/examples.scala) +``` +@@@ + +The result will be rendered like this (try to switch tabs): + +First-java +: @@snip [example-first.java](../../resources/tab-switching/examples.java) { #java_first group=java } + +First-scala +: @@snip [example-first.scala](../../resources/tab-switching/examples.scala) { #scala_first group=scala } + +Some separator. + +Java +: @@snip [example-second.java](../../resources/tab-switching/examples.java) + +Scala +: @@snip [example-second.scala](../../resources/tab-switching/examples.scala) + +This is also synced if some tabs have no snippet: + +Java +: @@snip [example-second.java](../../resources/tab-switching/examples.java) + +Scala +: More inline tabbing + ### snip.*.base_dir In order to specify your snippet source paths off certain base directories you can define placeholders diff --git a/docs/src/main/paradox/index.md b/docs/src/main/paradox/index.md index bad7321b..46d102de 100644 --- a/docs/src/main/paradox/index.md +++ b/docs/src/main/paradox/index.md @@ -54,6 +54,7 @@ which basically means that all of our extensions would start with `@` (for inlin * [Multi Configuration](features/multi-configuration.md) * [Organizing pages](features/organizing-pages.md) * [Linking](features/linking.md) +* [Groups](features/groups.md) * [Snippet inclusion](features/snippet-inclusion.md) * [Callouts](features/callouts.md) * [CSS Friendliness](features/css-friendliness.md) diff --git a/docs/src/main/resources/tab-switching/examples.java b/docs/src/main/resources/tab-switching/examples.java new file mode 100644 index 00000000..349e7bee --- /dev/null +++ b/docs/src/main/resources/tab-switching/examples.java @@ -0,0 +1,15 @@ +// #java_first +class JavaFirst { + public static void main(String[] args) { + System.out.println("Hello java!"); + } +} +// #java_first + +// #java_second +class JavaSecond { + public static void main(String[] args) { + System.out.println("Hello java!"); + } +} +// #java_second \ No newline at end of file diff --git a/docs/src/main/resources/tab-switching/examples.scala b/docs/src/main/resources/tab-switching/examples.scala new file mode 100644 index 00000000..b823ae87 --- /dev/null +++ b/docs/src/main/resources/tab-switching/examples.scala @@ -0,0 +1,11 @@ +// #scala_first +object ScalaFirst extends App { + println("Hello scala!") +} +// #scala_first + +// #scala_second +object ScalaSecond extends App { + println("Hello scala!") +} +// #scala_second \ No newline at end of file diff --git a/plugin/src/main/scala/com/lightbend/paradox/sbt/ParadoxKeys.scala b/plugin/src/main/scala/com/lightbend/paradox/sbt/ParadoxKeys.scala index 117345dd..896971cc 100644 --- a/plugin/src/main/scala/com/lightbend/paradox/sbt/ParadoxKeys.scala +++ b/plugin/src/main/scala/com/lightbend/paradox/sbt/ParadoxKeys.scala @@ -36,4 +36,5 @@ trait ParadoxKeys { val paradoxDefaultTemplateName = settingKey[String]("Name of default template for generating pages.") val paradoxTemplate = taskKey[PageTemplate]("PageTemplate to use when generating HTML pages.") val paradoxVersion = settingKey[String]("Paradox plugin version.") + val paradoxGroups = settingKey[Map[String, Seq[String]]]("Paradox groups.") } diff --git a/plugin/src/main/scala/com/lightbend/paradox/sbt/ParadoxPlugin.scala b/plugin/src/main/scala/com/lightbend/paradox/sbt/ParadoxPlugin.scala index b66903b8..9718a7a8 100644 --- a/plugin/src/main/scala/com/lightbend/paradox/sbt/ParadoxPlugin.scala +++ b/plugin/src/main/scala/com/lightbend/paradox/sbt/ParadoxPlugin.scala @@ -47,6 +47,7 @@ object ParadoxPlugin extends AutoPlugin { paradoxTheme := Some(builtinParadoxTheme("generic")), paradoxDefaultTemplateName := "page", paradoxLeadingBreadcrumbs := Nil, + paradoxGroups := Map.empty, libraryDependencies ++= paradoxTheme.value.toSeq ) @@ -126,6 +127,7 @@ object ParadoxPlugin extends AutoPlugin { (target in paradoxMarkdownToHtml).value, paradoxSourceSuffix.value, paradoxTargetSuffix.value, + paradoxGroups.value, paradoxProperties.value, paradoxNavigationDepth.value, paradoxTemplate.value, diff --git a/plugin/src/sbt-test/paradox/snippets/expected/group.html b/plugin/src/sbt-test/paradox/snippets/expected/group.html new file mode 100644 index 00000000..c7113739 --- /dev/null +++ b/plugin/src/sbt-test/paradox/snippets/expected/group.html @@ -0,0 +1,2 @@ +
private class SomethingElse
+
import scala.util.Try
\ No newline at end of file diff --git a/plugin/src/sbt-test/paradox/snippets/src/main/paradox/group.md b/plugin/src/sbt-test/paradox/snippets/src/main/paradox/group.md new file mode 100644 index 00000000..7aa50108 --- /dev/null +++ b/plugin/src/sbt-test/paradox/snippets/src/main/paradox/group.md @@ -0,0 +1,3 @@ +@@ snip [Group A](../../test/scala/Multiple.scala) { #multiple-something-else group=a } + +@@ snip [Group B](../../test/scala/Multiple.scala) { #parseint-imports group=b } diff --git a/plugin/src/sbt-test/paradox/snippets/test b/plugin/src/sbt-test/paradox/snippets/test index 186f2c70..838b93a7 100644 --- a/plugin/src/sbt-test/paradox/snippets/test +++ b/plugin/src/sbt-test/paradox/snippets/test @@ -6,3 +6,4 @@ $ must-mirror target/paradox/site/main/multiple.html expected/multiple.html $ must-mirror target/paradox/site/main/some-xml.html expected/some-xml.html $ must-mirror target/paradox/site/main/nocode.html expected/nocode.html $ must-mirror target/paradox/site/main/configured-bases.html expected/configured-bases.html +$ must-mirror target/paradox/site/main/group.html expected/group.html diff --git a/themes/generic/src/main/assets/groups.st b/themes/generic/src/main/assets/groups.st new file mode 100644 index 00000000..7358d501 --- /dev/null +++ b/themes/generic/src/main/assets/groups.st @@ -0,0 +1 @@ +$page.groups$ \ No newline at end of file diff --git a/themes/generic/src/main/assets/js/groups.js b/themes/generic/src/main/assets/js/groups.js new file mode 100644 index 00000000..20565f23 --- /dev/null +++ b/themes/generic/src/main/assets/js/groups.js @@ -0,0 +1,171 @@ +groupChangeListeners = []; + +window.groupChanged = function(callback) { + groupChangeListeners.push(callback); +} + +$(function() { + + // Groups (like 'java' and 'scala') represent groups of 'switchable' content, either in tabs or in regular text. + // The catalog of groups can be defined in the sbt parameters to initialize the group. + + var groupCookie = "paradoxGroups"; + var cookieTg = getCookie(groupCookie); + var currentGroups = {}; + + var catalog = {} + var supergroupByGroup = {}; + + + if(cookieTg != "") + currentGroups = JSON.parse(cookieTg); + + // http://www.w3schools.com/js/js_cookies.asp + function setCookie(cname,cvalue,exdays) { + if(!exdays) exdays = 365; + var d = new Date(); + d.setTime(d.getTime() + (exdays*24*60*60*1000)); + var expires = "expires=" + d.toGMTString(); + document.cookie = cname + "=" + cvalue + ";" + expires + ";path=/"; + } + + // http://www.w3schools.com/js/js_cookies.asp + function getCookie(cname) { + var name = cname + "="; + var decodedCookie = decodeURIComponent(document.cookie); + var ca = decodedCookie.split(';'); + for(var i = 0; i < ca.length; i++) { + var c = ca[i]; + while (c.charAt(0) == ' ') { + c = c.substring(1); + } + if (c.indexOf(name) == 0) { + return c.substring(name.length, c.length); + } + } + return ""; + } + + $("dl").has("dd > pre").each(function() { + var dl = $(this); + dl.addClass("tabbed"); + var dts = dl.find("dt"); + dts.each(function(i) { + var dt = $(this); + dt.html("" + dt.text() + ""); + }); + var dds = dl.find("dd"); + dds.each(function(i) { + var dd = $(this); + dd.hide(); + if (dd.find("blockquote").length) { + dd.addClass("has-note"); + } + }); + + // Default to the first tab, for grouped tabs switch again later + switchToTab(dts.first()); + + dts.first().addClass("first"); + dts.last().addClass("last"); + }); + + $(".supergroup").each(function() { + var supergroup = $(this).attr('name').toLowerCase(); + var groups = $(this).find(".group"); + + var current = currentGroups[supergroup]; + if (!current) { + current = "group-" + groups.first().text().toLowerCase(); + currentGroups[supergroup] = current; + } + + catalog[supergroup] = []; + + groups.each(function() { + var group = "group-" + $(this).text().toLowerCase(); + catalog[supergroup].push(group); + supergroupByGroup[group] = supergroup; + }); + + switchToGroup(supergroup, current); + + $(this).on("change", function() { + switchToGroup(supergroup, this.value); + }); + }); + + $("dl.tabbed dt a").click(function(e){ + e.preventDefault(); + var currentDt = $(this).parent("dt"); + var currentDl = currentDt.parent("dl"); + + var currentGroup = groupOf(currentDt); + + var supergroup = supergroupByGroup[currentGroup] + if (supergroup) { + switchToGroup(supergroup, currentGroup); + } else { + switchToTab(currentDt); + } + }); + + function switchToGroup(supergroup, group) { + currentGroups[supergroup] = group; + setCookie(groupCookie, JSON.stringify(currentGroups)); + + // Dropdown switcher: + $("select") + .has("option[value=" + group +"]") + .val(group); + + // Inline snippets: + for (var i = 0; i < catalog[supergroup].length; i++) { + var peer = catalog[supergroup][i]; + if (peer == group) { + $("span." + group).show(); + } else { + $("span." + peer).hide(); + } + } + + // Tabbed snippets: + $("dl.tabbed").each(function() { + var dl = $(this); + dl.find("dt").each(function() { + var dt = $(this); + if(groupOf(dt) == group) { + switchToTab(dt); + } + }); + }); + + for (var i = 0; i < groupChangeListeners.length; i++) { + groupChangeListeners[i](group, supergroup, catalog); + } + } + + function switchToTab(dt) { + var dl = dt.parent("dl"); + dl.find(".current").removeClass("current").next("dd").removeClass("current").hide(); + dt.addClass("current"); + var currentContent = dt.next("dd").addClass("current").show(); + dl.css("height", dt.height() + currentContent.height()); + } + + function groupOf(elem) { + var classAttribute = elem.next("dd").find("pre").attr("class"); + if (classAttribute) { + var currentClasses = classAttribute.split(' '); + var regex = new RegExp("^group-.*"); + for(var i = 0; i < currentClasses.length; i++) { + if(regex.test(currentClasses[i])) { + return currentClasses[i]; + } + } + } + + // No class found? Then use the tab title + return "group-" + elem.find('a').text().toLowerCase(); + } +}); \ No newline at end of file diff --git a/themes/generic/src/main/assets/js/page.js b/themes/generic/src/main/assets/js/page.js index 4b4daf48..cb2ef674 100644 --- a/themes/generic/src/main/assets/js/page.js +++ b/themes/generic/src/main/assets/js/page.js @@ -1,37 +1,2 @@ $(function() { - - // Tabbed code samples - - $("dl").has("dd > pre").each(function() { - var dl = $(this); - dl.addClass("tabbed"); - var dts = dl.find("dt"); - dts.each(function(i) { - var dt = $(this); - dt.html("" + dt.text() + ""); - }); - var dds = dl.find("dd"); - dds.each(function(i) { - var dd = $(this); - dd.hide(); - if (dd.find("blockquote").length) { - dd.addClass("has-note"); - } - }); - var current = dts.first().addClass("first").addClass("current"); - var currentContent = current.next("dd").addClass("current").show(); - dts.last().addClass("last"); - dl.css("height", current.height() + currentContent.height()); - }); - - $("dl.tabbed dt a").click(function(e){ - e.preventDefault(); - var current = $(this).parent("dt"); - var dl = current.parent("dl"); - dl.find(".current").removeClass("current").next("dd").removeClass("current").hide(); - current.addClass("current"); - var currentContent = current.next("dd").addClass("current").show(); - dl.css("height", current.height() + currentContent.height()); - }); - -}); +}); \ No newline at end of file diff --git a/themes/generic/src/main/assets/navigation.st b/themes/generic/src/main/assets/navigation.st index cd45efc5..6441077d 100644 --- a/themes/generic/src/main/assets/navigation.st +++ b/themes/generic/src/main/assets/navigation.st @@ -6,6 +6,7 @@ $page.properties.("project.version.short")$ +$groups()$ diff --git a/themes/generic/src/main/assets/page.st b/themes/generic/src/main/assets/page.st index 1cc6ac9a..0bec374c 100644 --- a/themes/generic/src/main/assets/page.st +++ b/themes/generic/src/main/assets/page.st @@ -9,6 +9,7 @@ +