Skip to content

Commit 6f16465

Browse files
authored
Support linking to method anchors (#81)
* Support linking to method anchors * Document the variations in the readme * Scala 2.12.10 * cache coursier dir
1 parent 0cdd3c6 commit 6f16465

File tree

5 files changed

+139
-23
lines changed

5 files changed

+139
-23
lines changed

.travis.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
language: scala
22
jdk: openjdk8
3-
scala: 2.12.8
3+
scala: 2.12.10
44

55
jobs:
66
include:
@@ -28,6 +28,7 @@ stages:
2828

2929
cache:
3030
directories:
31+
- $HOME/.cache/coursier
3132
- $HOME/.ivy2/cache
3233
- $HOME/.sbt
3334

README.md

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,60 @@ You can now create 'grouped' javadoc/scaladoc references in paradox like:
1919
Look into the documentation for @apidoc[MyClass].
2020
```
2121

22-
This will automatically find the FQDN of the class when it is unique. When the
22+
This will automatically find the FQCN of the class when it is unique. When the
2323
name does not uniquely identify the class, the plugin will check for the
2424
`scaladsl`/`javadsl` package convention found in Akka projects. If that doesn't
25-
produce an unambigious result, you will have to use the FQDN.
25+
produce an unambigious result, you will have to use the FQCN.
26+
27+
## Examples
28+
29+
[See details in the tests](/src/test/scala/com/lightbend/paradox/apidoc/ApidocDirectiveSpec.scala)
30+
31+
* `@apidoc[Actor]` (Just one class exists.)
32+
* classes: `akka.actor.Actor`
33+
* Scala: `Actor` - `akka/actor/Actor.html`
34+
* Java: `Actor` - `akka/actor/Actor.html`
35+
36+
* `@apidoc[Flow]` (Both scaladoc and javadoc exist.)
37+
* classes: `akka.stream.scaladsl.Flow` - `akka.stream.javadsl.Flow`
38+
* Scala: Flow - `akka/stream/scaladsl/Flow.html`
39+
* Java: Flow - `akka/stream/javadsl/Flow.html`
40+
41+
* `@apidoc[Marshaller]` (The scaladoc/javadoc split can be on different package depth.)
42+
* classes: `akka.http.scaladsl.marshalling.Marshaller`, `akka.http.javadsl.marshalling.Marshaller`
43+
* Scala: Marshaller - `akka/http/scaladsl/marshalling/Marshaller.html`
44+
* Java: Marshaller - `akka/http/javadsl/marshalling/Marshaller.html`
45+
46+
* `@apidoc[typed.*.Replicator$]` (The classes exist in multiple places.)
47+
* classes: `akka.cluster.ddata.Replicator$`, `akka.cluster.ddata.typed.scaladsl.Replicator$`, `akka.cluster.ddata.typed.javadsl.Replicator$`
48+
` * Scala: Replicator - `akka/cluster/ddata/typed/scaladsl/Replicator$.html`
49+
* Java: Replicator - `akka/cluster/ddata/typed/javadsl/Replicator.html`
50+
51+
* `@apidoc[akka.stream.(javadsl|scaladsl).Concat]` (The classes exist in even more places.)
52+
* classes: `akka.stream.impl.Concat`, `akka.stream.scaladsl.Concat$`, `akka.stream.scaladsl.Concat`, `akka.stream.javadsl.Concat$`
53+
* Scala: Concat - `akka/stream/scaladsl/Concat.html`
54+
* Java: Concat - `akka/stream/javadsl/Concat.html`
55+
56+
* `@apidoc[ClusterClient$]` (Link to scala object.)
57+
* classes: `akka.cluster.client.ClusterClient`
58+
* Scala: ClusterClient - `akka/cluster/client/ClusterClient$.html`
59+
* Java: ClusterClient - `akka/cluster/client/ClusterClient.html`
60+
61+
* `@apidoc[Source[ServerSentEvent, \_]]` (Show type paramters.)
62+
* classes: `akka.stream.scaladsl.Source` - `akka.stream.javadsl.Source`
63+
* Scala: Source\[ServerSentEvent, _\] - `akka/stream/scaladsl/Source.html`
64+
* Java: Source\<ServerSentEvent, ?\> - `akka/stream/javadsl/Source.html`
65+
66+
* `@apidoc[TheClass.method](Flow)` (Different link text than the class name.)
67+
* classes: `akka.stream.scaladsl.Flow` - `akka.stream.javadsl.Flow`
68+
* Scala: TheClass.method<br>`akka/stream/scaladsl/Flow.html`
69+
* Java: TheClass.method<br>`akka/stream/javadsl/Flow.html`
70+
71+
* `@apidoc[method](Flow) { scala="#method():Unit" java="#method()" }` (Link to method anchors.)
72+
* classes: `akka.stream.scaladsl.Flow` - `akka.stream.javadsl.Flow`
73+
* Scala: method - `akka/stream/scaladsl/Flow.html#method():Unit`
74+
* Java: method - `akka/stream/javadsl/Flow.html#method()`
75+
2676

2777
To limit the packages that are searched for classes, configure the
2878
`apidocRootPackage` setting:

build.sbt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import scala.collection.JavaConverters._
22

3-
scalaVersion := "2.12.9"
3+
scalaVersion := "2.12.10"
44

55
sbtPlugin := true
66
crossSbtVersions := List("1.0.0")

src/main/scala/com/lightbend/paradox/apidoc/ApidocDirective.scala

Lines changed: 42 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package com.lightbend.paradox.apidoc
1818

1919
import com.lightbend.paradox.markdown.InlineDirective
2020
import org.pegdown.Printer
21+
import org.pegdown.ast.DirectiveNode.Source
2122
import org.pegdown.ast.{DirectiveNode, Visitor}
2223

2324
class ApidocDirective(allClassesAndObjects: IndexedSeq[String], properties: Map[String, String])
@@ -29,9 +30,13 @@ class ApidocDirective(allClassesAndObjects: IndexedSeq[String], properties: Map[
2930

3031
val allClasses = allClassesAndObjects.filterNot(_.endsWith("$"))
3132

32-
private case class Query(pattern: String, generics: String, linkToObject: Boolean) {
33+
private case class Query(label: Option[String], pattern: String, generics: String, linkToObject: Boolean) {
3334
def scalaLabel(matched: String): String =
34-
matched.split('.').last + generics
35+
label match {
36+
case None => matched.split('.').last + generics
37+
case Some(la) => la + generics
38+
}
39+
3540
def javaLabel(matched: String): String =
3641
scalaLabel(matched)
3742
.replaceAll("\\[", "<")
@@ -44,19 +49,33 @@ class ApidocDirective(allClassesAndObjects: IndexedSeq[String], properties: Map[
4449
}
4550
private object Query {
4651
def apply(label: String): Query = {
47-
val (pattern, generics) = label.indexOf('[') match {
48-
case -1 => (label, "")
49-
case n => label.replaceAll("\\\\_", "_").splitAt(n)
50-
}
52+
val (pattern, generics) = splitGenerics(label)
53+
if (pattern.endsWith("$"))
54+
Query(None, pattern.init, generics, linkToObject = true)
55+
else
56+
Query(None, pattern, generics, linkToObject = false)
57+
}
58+
59+
def apply(label: String, pattern: String): Query = {
60+
val (labelPattern, generics) = splitGenerics(label)
5161
if (pattern.endsWith("$"))
52-
Query(pattern.init, generics, linkToObject = true)
62+
Query(Some(labelPattern), pattern.init, generics, linkToObject = true)
5363
else
54-
Query(pattern, generics, linkToObject = false)
64+
Query(Some(labelPattern), pattern, generics, linkToObject = false)
5565
}
66+
67+
private def splitGenerics(label: String): (String, String) =
68+
label.indexOf('[') match {
69+
case -1 => (label, "")
70+
case n => label.replaceAll("\\\\_", "_").splitAt(n)
71+
}
5672
}
5773

5874
def render(node: DirectiveNode, visitor: Visitor, printer: Printer): Unit = {
59-
val query = Query(node.label)
75+
val query = node.source match {
76+
case Source.Empty | _: Source.Ref => Query(node.label)
77+
case s: Source.Direct => Query(node.label, s.value)
78+
}
6079
if (query.pattern.contains('.')) {
6180
if (allClasses.contains(query.pattern)) {
6281
renderMatches(query, Seq(query.pattern), node, visitor, printer)
@@ -84,6 +103,7 @@ class ApidocDirective(allClassesAndObjects: IndexedSeq[String], properties: Map[
84103
doctype: String,
85104
label: String,
86105
fqcn: String,
106+
anchor: String,
87107
node: DirectiveNode
88108
): DirectiveNode = {
89109
val attributes = new org.pegdown.ast.DirectiveAttributes.AttributeMap()
@@ -98,7 +118,7 @@ class ApidocDirective(allClassesAndObjects: IndexedSeq[String], properties: Map[
98118
DirectiveNode.Format.Inline,
99119
doctype + "doc",
100120
label,
101-
new DirectiveNode.Source.Direct(fqcn),
121+
new DirectiveNode.Source.Direct(fqcn + anchor),
102122
node.attributes,
103123
label, // contents
104124
null
@@ -114,6 +134,8 @@ class ApidocDirective(allClassesAndObjects: IndexedSeq[String], properties: Map[
114134
printer: Printer
115135
): Unit = {
116136
val scalaClassSuffix = if (query.linkToObject) "$" else ""
137+
val sAnchor = node.attributes.value("scala", "")
138+
val jAnchor = node.attributes.value("java", "")
117139

118140
matches.size match {
119141
case 0 =>
@@ -122,20 +144,22 @@ class ApidocDirective(allClassesAndObjects: IndexedSeq[String], properties: Map[
122144
throw new java.lang.IllegalStateException(s"Match for $query only found in one language: ${matches(0)}")
123145
case 1 =>
124146
val pkg = matches(0)
125-
syntheticNode("scala", "scala", query.scalaLabel(pkg), pkg + scalaClassSuffix, node).accept(visitor)
126-
if (hasJavadocUrl(pkg))
127-
syntheticNode("java", "java", query.javaLabel(pkg), pkg, node).accept(visitor)
128-
else
129-
syntheticNode("java", "scala", query.scalaLabel(pkg), pkg + scalaClassSuffix, node).accept(visitor)
147+
syntheticNode("scala", "scala", query.scalaLabel(pkg), pkg + scalaClassSuffix, sAnchor, node).accept(visitor)
148+
if (hasJavadocUrl(pkg)) {
149+
syntheticNode("java", "java", query.javaLabel(pkg), pkg, jAnchor, node).accept(visitor)
150+
} else
151+
syntheticNode("java", "scala", query.scalaLabel(pkg), pkg + scalaClassSuffix, jAnchor, node).accept(visitor)
130152
case 2 if matches.forall(_.contains("adsl")) =>
131153
matches.foreach(pkg => {
132154
if (!pkg.contains("javadsl"))
133-
syntheticNode("scala", "scala", query.scalaLabel(pkg), pkg + scalaClassSuffix, node).accept(visitor)
155+
syntheticNode("scala", "scala", query.scalaLabel(pkg), pkg + scalaClassSuffix, sAnchor, node)
156+
.accept(visitor)
134157
if (!pkg.contains("scaladsl")) {
135158
if (hasJavadocUrl(pkg))
136-
syntheticNode("java", "java", query.javaLabel(pkg), pkg, node).accept(visitor)
159+
syntheticNode("java", "java", query.javaLabel(pkg), pkg, jAnchor, node).accept(visitor)
137160
else
138-
syntheticNode("java", "scala", query.scalaLabel(pkg), pkg + scalaClassSuffix, node).accept(visitor)
161+
syntheticNode("java", "scala", query.scalaLabel(pkg), pkg + scalaClassSuffix, jAnchor, node)
162+
.accept(visitor)
139163
}
140164
})
141165
case n =>

src/test/scala/com/lightbend/paradox/apidoc/ApidocDirectiveSpec.scala

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ class ApidocDirectiveSpec extends MarkdownBaseSpec {
5959
)
6060

6161
implicit val context = writerContextWithProperties(
62+
"javadoc.link_style" -> "frames",
6263
"scaladoc.akka.base_url" -> "https://doc.akka.io/api/akka/2.5",
6364
"scaladoc.akka.http.base_url" -> "https://doc.akka.io/api/akka-http/current",
6465
"javadoc.akka.base_url" -> "https://doc.akka.io/japi/akka/2.5",
@@ -127,7 +128,7 @@ class ApidocDirectiveSpec extends MarkdownBaseSpec {
127128
)
128129
}
129130

130-
it should "find a class by partiql fqdn" in {
131+
it should "find a class by partial fqcn" in {
131132
markdown("@apidoc[actor.typed.ActorRef]") shouldEqual
132133
html(
133134
"""<p><span class="group-scala">
@@ -176,4 +177,44 @@ class ApidocDirectiveSpec extends MarkdownBaseSpec {
176177
|</p>""".stripMargin
177178
)
178179
}
180+
181+
"Anchor attributes" should "be used" in {
182+
markdown("""The @apidoc[Flow] { scala="#method():Unit" java="#method()" } thingie""") shouldEqual
183+
html(
184+
"""<p>The <span class="group-java">
185+
|<a href="https://doc.akka.io/japi/akka/2.5/?akka/stream/javadsl/Flow.html#method()" title="akka.stream.javadsl.Flow"><code>Flow</code></a></span><span class="group-scala">
186+
|<a href="https://doc.akka.io/api/akka/2.5/akka/stream/scaladsl/Flow.html#method():Unit" title="akka.stream.scaladsl.Flow"><code>Flow</code></a></span>
187+
|thingie</p>""".stripMargin
188+
)
189+
}
190+
191+
"Directive with label and source" should "use the source as class pattern" in {
192+
markdown("The @apidoc[TheClass.method](Flow) { .scaladoc a=1 } thingie") shouldEqual
193+
html(
194+
"""<p>The <span class="group-java">
195+
|<a href="https://doc.akka.io/japi/akka/2.5/?akka/stream/javadsl/Flow.html" title="akka.stream.javadsl.Flow"><code>TheClass.method</code></a></span><span class="group-scala">
196+
|<a href="https://doc.akka.io/api/akka/2.5/akka/stream/scaladsl/Flow.html" title="akka.stream.scaladsl.Flow"><code>TheClass.method</code></a></span>
197+
|thingie</p>""".stripMargin
198+
)
199+
}
200+
201+
it should "adapt generics notation from the label" in {
202+
markdown("The @apidoc[TheClass[File].method[String]](Flow) { .scaladoc a=1 } thingie") shouldEqual
203+
html(
204+
"""<p>The <span class="group-java">
205+
|<a href="https://doc.akka.io/japi/akka/2.5/?akka/stream/javadsl/Flow.html" title="akka.stream.javadsl.Flow"><code>TheClass&lt;File&gt;.method&lt;String&gt;</code></a></span><span class="group-scala">
206+
|<a href="https://doc.akka.io/api/akka/2.5/akka/stream/scaladsl/Flow.html" title="akka.stream.scaladsl.Flow"><code>TheClass[File].method[String]</code></a></span>
207+
|thingie</p>""".stripMargin
208+
)
209+
}
210+
211+
it should "use anchors" in {
212+
markdown("""The @apidoc[TheClass[File].method[String]](Flow) { scala="#method():Unit" java="#method()" } thingie""") shouldEqual
213+
html(
214+
"""<p>The <span class="group-java">
215+
|<a href="https://doc.akka.io/japi/akka/2.5/?akka/stream/javadsl/Flow.html#method()" title="akka.stream.javadsl.Flow"><code>TheClass&lt;File&gt;.method&lt;String&gt;</code></a></span><span class="group-scala">
216+
|<a href="https://doc.akka.io/api/akka/2.5/akka/stream/scaladsl/Flow.html#method():Unit" title="akka.stream.scaladsl.Flow"><code>TheClass[File].method[String]</code></a></span>
217+
|thingie</p>""".stripMargin
218+
)
219+
}
179220
}

0 commit comments

Comments
 (0)