Skip to content

Commit b2a4f65

Browse files
committed
Add UnidocDirective implementation
1 parent f02eeff commit b2a4f65

File tree

5 files changed

+384
-1
lines changed

5 files changed

+384
-1
lines changed
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/**
2+
* Copyright (C) 2009-2018 Lightbend Inc. <https://www.lightbend.com>
3+
*/
4+
5+
package com.lightbend.paradox.unidoc
6+
7+
import com.lightbend.paradox.markdown.InlineDirective
8+
import org.pegdown.Printer
9+
import org.pegdown.ast.{DirectiveNode, TextNode, Visitor}
10+
11+
class UnidocDirective(allClasses: IndexedSeq[String]) extends InlineDirective("unidoc") {
12+
def render(node: DirectiveNode, visitor: Visitor, printer: Printer): Unit = {
13+
if (node.label.split('[')(0).contains('.')) {
14+
val fqcn = node.label
15+
if (allClasses.contains(fqcn)) {
16+
val label = fqcn.split('.').last
17+
syntheticNode("scala", scalaLabel(label), fqcn, node).accept(visitor)
18+
syntheticNode("java", javaLabel(label), fqcn, node).accept(visitor)
19+
} else {
20+
throw new java.lang.IllegalStateException(s"fqcn not found by @unidoc[$fqcn]")
21+
}
22+
}
23+
else {
24+
renderByClassName(node.label, node, visitor, printer)
25+
}
26+
}
27+
28+
private def baseClassName(label: String) = {
29+
val labelWithoutGenerics = label.split("\\[")(0)
30+
if (labelWithoutGenerics.endsWith("$")) labelWithoutGenerics.init
31+
else labelWithoutGenerics
32+
}
33+
34+
def javaLabel(label: String): String =
35+
scalaLabel(label).replaceAll("\\[", "&lt;").replaceAll("\\]", "&gt;").replace('_', '?')
36+
37+
def scalaLabel(label: String): String =
38+
if (label.endsWith("$")) label.init
39+
else label
40+
41+
def syntheticNode(group: String, label: String, fqcn: String, node: DirectiveNode): DirectiveNode = {
42+
val syntheticSource = new DirectiveNode.Source.Direct(fqcn)
43+
val attributes = new org.pegdown.ast.DirectiveAttributes.AttributeMap()
44+
new DirectiveNode(DirectiveNode.Format.Inline, group, null, null, attributes, null,
45+
new DirectiveNode(DirectiveNode.Format.Inline, group + "doc", label, syntheticSource, node.attributes, fqcn,
46+
new TextNode(label)
47+
))
48+
}
49+
50+
def renderByClassName(label: String, node: DirectiveNode, visitor: Visitor, printer: Printer): Unit = {
51+
val query = node.label.replaceAll("\\\\_", "_")
52+
val className = baseClassName(query)
53+
val classSuffix = if (query.endsWith("$")) "$" else ""
54+
55+
val matches = allClasses.filter(_.endsWith('.' + className))
56+
matches.size match {
57+
case 0 =>
58+
throw new java.lang.IllegalStateException(s"No matches found for $query")
59+
case 1 if matches(0).contains("adsl") =>
60+
throw new java.lang.IllegalStateException(s"Match for $query only found in one language: ${matches(0)}")
61+
case 1 =>
62+
syntheticNode("scala", scalaLabel(query), matches(0) + classSuffix, node).accept(visitor)
63+
syntheticNode("java", javaLabel(query), matches(0) + classSuffix, node).accept(visitor)
64+
case 2 if matches.forall(_.contains("adsl")) =>
65+
matches.foreach(m => {
66+
if (!m.contains("javadsl"))
67+
syntheticNode("scala", scalaLabel(query), m + classSuffix, node).accept(visitor)
68+
if (!m.contains("scaladsl"))
69+
syntheticNode("java", javaLabel(query), m + classSuffix, node).accept(visitor)
70+
})
71+
case n =>
72+
throw new java.lang.IllegalStateException(
73+
s"$n matches found for $query, but not javadsl/scaladsl: ${matches.mkString(", ")}. " +
74+
s"You may want to use the fully qualified class name as @unidoc[fqcn] instead of @unidoc[${label}]."
75+
)
76+
}
77+
}
78+
79+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/**
2+
* Copyright (C) 2009-2018 Lightbend Inc. <https://www.lightbend.com>
3+
*/
4+
5+
package com.lightbend.paradox.unidoc
6+
7+
import sbt._
8+
9+
object UnidocKeys {
10+
val unidocRootPackage = settingKey[String]("")
11+
}

src/main/scala/com/lightbend/paradox/unidoc/UnidocPlugin.scala

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,19 @@
44

55
package com.lightbend.paradox.unidoc
66

7+
import _root_.io.github.lukehutch.fastclasspathscanner.FastClasspathScanner
8+
import com.lightbend.paradox.markdown.Writer
79
import com.lightbend.paradox.sbt.ParadoxPlugin
10+
import com.lightbend.paradox.sbt.ParadoxPlugin.autoImport.paradoxDirectives
11+
import sbt.Keys.fullClasspath
812
import sbt._
913

14+
import scala.collection.JavaConverters._
15+
1016
object UnidocPlugin extends AutoPlugin {
17+
import UnidocKeys._
18+
19+
val version = ParadoxPlugin.readProperty("akka-paradox.properties", "akka.paradox.version")
1120

1221
override def requires: Plugins = ParadoxPlugin
1322

@@ -16,6 +25,16 @@ object UnidocPlugin extends AutoPlugin {
1625
override def projectSettings: Seq[Setting[_]] = unidocSettings(Compile)
1726

1827
def unidocParadoxGlobalSettings: Seq[Setting[_]] = Seq(
28+
unidocRootPackage := "scala",
29+
paradoxDirectives ++= Def.taskDyn {
30+
val classpath = (fullClasspath in Compile).value.files.map(_.toURI.toURL).toArray
31+
val classLoader = new java.net.URLClassLoader(classpath, this.getClass.getClassLoader)
32+
val scanner = new FastClasspathScanner(unidocRootPackage.value).addClassLoader(classLoader).scan()
33+
val allClasses = scanner.getNamesOfAllClasses.asScala.toVector
34+
Def.task { Seq(
35+
{ _: Writer.Context new UnidocDirective(allClasses) }
36+
)}
37+
}.value
1938
)
2039

2140
def unidocSettings(config: Configuration): Seq[Setting[_]] = unidocParadoxGlobalSettings ++ inConfig(config)(Seq(
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
/**
2+
* Copyright (C) 2009-2018 Lightbend Inc. <https://www.lightbend.com>
3+
*/
4+
5+
6+
package com.lightbend.paradox.unidoc
7+
8+
import com.lightbend.paradox.tree.Tree.{Forest, Location}
9+
import java.io.{File, PrintWriter}
10+
11+
import org.scalatest.{FlatSpec, Matchers}
12+
import com.lightbend.paradox.template.PageTemplate
13+
import java.nio.file._
14+
15+
import com.lightbend.paradox.markdown._
16+
17+
abstract class MarkdownBaseSpec extends FlatSpec with Matchers {
18+
19+
val markdownReader = new Reader
20+
val markdownWriter = new Writer
21+
22+
def markdown(text: String)(implicit context: Location[Page] => Writer.Context = writerContext): String = {
23+
markdownPages("test.md" -> text).getOrElse("test.html", "")
24+
}
25+
26+
def markdownPages(mappings: (String, String)*)(implicit context: Location[Page] => Writer.Context = writerContext): Map[String, String] = {
27+
def render(location: Option[Location[Page]], rendered: Seq[(String, String)] = Seq.empty): Seq[(String, String)] = location match {
28+
case Some(loc) =>
29+
val page = loc.tree.label
30+
val html = normalize(markdownWriter.write(page.markdown, context(loc)))
31+
render(loc.next, rendered :+ (page.path, html))
32+
case None => rendered
33+
}
34+
render(Location.forest(pages(mappings: _*))).toMap
35+
}
36+
37+
def layoutPages(mappings: (String, String)*)(templates: (String, String)*)(implicit context: Location[Page] => Writer.Context = writerContext): Map[String, String] = {
38+
val templateDirectory = Files.createTempDirectory("templates")
39+
createFileTemplates(templateDirectory, templates)
40+
def render(location: Option[Location[Page]], rendered: Seq[(String, String)] = Seq.empty): Seq[(String, String)] = location match {
41+
case Some(loc) =>
42+
val page = loc.tree.label
43+
val html = normalize(markdownWriter.write(page.markdown, context(loc)))
44+
val outputFile = new File(page.path)
45+
val emptyPageContext = PartialPageContent(page.properties.get, html)
46+
val template = new PageTemplate(new File(templateDirectory.toString))
47+
template.write(page.properties(Page.Properties.DefaultLayoutMdIndicator, template.defaultName), emptyPageContext, outputFile, new PageTemplate.ErrorLogger(s => println("[error] " + s)))
48+
val fileContent = fileToContent(outputFile)
49+
outputFile.delete
50+
render(loc.next, rendered :+ (page.path, normalize(fileContent)))
51+
case None => rendered
52+
}
53+
render(Location.forest(pages(mappings: _*))).toMap
54+
}
55+
56+
def fileToContent(file: File): String = {
57+
import scala.io.Source
58+
Source.fromFile(file).getLines.mkString("\n")
59+
}
60+
61+
def createFileTemplates(dir: Path, templates: Seq[(String, String)]) = {
62+
val suffix = ".st"
63+
(templates map {
64+
case (path, content) if (path.endsWith(suffix)) =>
65+
val writer = new PrintWriter(new File(dir.toString + "/" + path))
66+
writer.write(prepare(content))
67+
writer.close()
68+
})
69+
}
70+
71+
def writerContextWithProperties(properties: (String, String)*): Location[Page] => Writer.Context = { location =>
72+
writerContext(location).copy(properties = properties.toMap)
73+
}
74+
75+
def writerContext(location: Location[Page]): Writer.Context = {
76+
Writer.Context(
77+
location,
78+
Page.allPaths(List(location.root.tree)).toSet,
79+
groups = Map("Language" -> Seq("Scala", "Java"))
80+
)
81+
}
82+
83+
def pages(mappings: (String, String)*): Forest[Page] = {
84+
import com.lightbend.paradox.markdown.Path
85+
val parsed = mappings map {
86+
case (path, text) =>
87+
val frontin = Frontin(prepare(text))
88+
(new File(path), path, markdownReader.read(frontin.body), frontin.header)
89+
}
90+
Page.forest(parsed, Path.replaceSuffix(Writer.DefaultSourceSuffix, Writer.DefaultTargetSuffix))
91+
}
92+
93+
def html(text: String): String = {
94+
normalize(prepare(text))
95+
}
96+
97+
def htmlPages(mappings: (String, String)*): Map[String, String] = {
98+
(mappings map { case (path, text) => (path, html(text)) }).toMap
99+
}
100+
101+
def prepare(text: String): String = {
102+
text.stripMargin.trim
103+
}
104+
105+
def normalize(html: String) = {
106+
val reader = new java.io.StringReader(html)
107+
val writer = new java.io.StringWriter
108+
val tidy = new org.w3c.tidy.Tidy
109+
tidy.setTabsize(2)
110+
tidy.setPrintBodyOnly(true)
111+
tidy.setTrimEmptyElements(false)
112+
tidy.setShowWarnings(false)
113+
tidy.setQuiet(true)
114+
tidy.parse(reader, writer)
115+
writer.toString.replace("\r\n", "\n").replace("\r", "\n")
116+
}
117+
118+
case class PartialPageContent(properties: Map[String, String], content: String) extends PageTemplate.Contents {
119+
import scala.collection.JavaConverters._
120+
121+
val getTitle = ""
122+
val getContent = content
123+
124+
lazy val getBase = ""
125+
lazy val getHome = new EmptyLink()
126+
lazy val getPrev = new EmptyLink()
127+
lazy val getNext = new EmptyLink()
128+
lazy val getBreadcrumbs = ""
129+
lazy val getNavigation = ""
130+
lazy val hasSubheaders = false
131+
lazy val getToc = ""
132+
lazy val getSource_url = ""
133+
134+
lazy val getProperties = properties.asJava
135+
}
136+
137+
case class EmptyLink() extends PageTemplate.Link {
138+
lazy val getHref: String = ""
139+
lazy val getHtml: String = ""
140+
lazy val getTitle: String = ""
141+
lazy val isActive: Boolean = false
142+
}
143+
144+
}

0 commit comments

Comments
 (0)