Skip to content

Commit 6f49d2b

Browse files
poWer4aiXraboof
authored andcommitted
API doc directives: accept full character range in package names
According to the [Java Language Specification](https://docs.oracle.com/javase/specs/jls/se8/html/index.html) member of packages, e.g. subpackages are identifiers. Identifiers itself is ...an unlimited-length sequence of Java letters and Java digits, the first of which must be a Java letter. As a `Java letter` includes more than what is expressed by the regex `\b[a-z][a-z0-9_]*)\.`, package names which include uppercase or non ASCII characters cannot be processed. I know that there are some _naming conventions_ flying around but something like `aBc.DE.fg` is still a valid package name in my opinion. I have changed the regex in `packageDotsToSlash()` to `(\b\p{javaJavaIdentifierStart}\p{javaJavaIdentifierPart}*)\.`. Unfortunately this breaks with the changes applied for issue #397, #395, #98, #86 to handle inner classes. In order to support this notation, the regex can be relaxed to `(\b\p{javaLowerCase}\p{javaJavaIdentifierPart}*)\.`. I have added the variable 'scaladoc.strictPackageIdent' (which defaults to `false`) by which you can switch between the two regexp. Once set to true, the package name can conform to an identifier now.
1 parent 1a84ca8 commit 6f49d2b

File tree

4 files changed

+242
-21
lines changed

4 files changed

+242
-21
lines changed

core/src/main/scala/com/lightbend/paradox/markdown/Directive.scala

Lines changed: 57 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -312,25 +312,54 @@ abstract class ApiDocDirective(name: String)
312312
}
313313

314314
object ApiDocDirective {
315-
/** This relies on the naming convention of packages being all-ascii-lowercase (which is rarely broken), numbers and underscore. */
316-
def packageDotsToSlash(s: String): String = s.replaceAll("(\\b[a-z][a-z0-9_]*)\\.", "$1/")
315+
/**
316+
* Converts package dot notation to a path, separated by '/'
317+
* Allow all valid java characters and java numbers to be used, according to the java lang spec.
318+
*
319+
* @param s package or full qualified class name to be converted.
320+
* @param packageNameStyle Setting `startWithLowercase`` will get it wrong when a package name
321+
* starts with an uppercase letter or when an inner class starts with
322+
* a lowercase character, while `startWithAnycase` will derive the wrong
323+
* path whenever an inner class is encountered.
324+
* @return Resulting path.
325+
*/
326+
def packageDotsToSlash(s: String, packageNameStyle: String): String =
327+
if (packageNameStyle == "startWithAnycase")
328+
s.replaceAll("(\\b\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*)\\.", "$1/")
329+
else
330+
s.replaceAll("(\\b\\p{javaLowerCase}\\p{javaJavaIdentifierPart}*)\\.", "$1/")
331+
}
332+
333+
object ScaladocDirective {
334+
final val ScaladocPackageNameStyleProperty = raw"""scaladoc\.(.*)\.package_name_style""".r
335+
336+
>>>>>>> 517023d (API doc directives: accept full character range in package names)
317337
}
318338

319339
case class ScaladocDirective(ctx: Writer.Context)
320340
extends ApiDocDirective("scaladoc") {
321341

342+
import ScaladocDirective._
343+
344+
val defaultPackageNameStyle = variables.getOrElse("scaladoc.package_name_style", "startWithLowercase")
345+
val packagePackageNameStyle: Map[String, String] = variables.collect {
346+
case (property @ ScaladocPackageNameStyleProperty(pkg), url) => (pkg, variables(property))
347+
}
348+
322349
protected def resolveApiLink(link: String): Url = {
323350
val levels = link.split("[.]")
324351
val packages = (1 to levels.init.length).map(levels.take(_).mkString("."))
325-
val baseUrl = packages.reverse.collectFirst(baseUrls).getOrElse(defaultBaseUrl).resolve()
326-
url(link, baseUrl)
352+
val packagesDeepestFirst = packages.reverse
353+
val baseUrl = packagesDeepestFirst.collectFirst(baseUrls).getOrElse(defaultBaseUrl).resolve()
354+
val packageNameStyle = packagesDeepestFirst.collectFirst(packagePackageNameStyle).getOrElse(defaultPackageNameStyle)
355+
url(link, baseUrl, packageNameStyle)
327356
}
328357

329358
private def classDotsToDollarDollar(s: String) = s.replaceAll("(\\b[A-Z].+)\\.", "$1\\$\\$")
330359

331-
private def url(link: String, baseUrl: Url): Url = {
360+
private def url(link: String, baseUrl: Url, packageNameStyle: String): Url = {
332361
val url = Url(link).base
333-
val path = classDotsToDollarDollar(ApiDocDirective.packageDotsToSlash(url.getPath)) + ".html"
362+
val path = classDotsToDollarDollar(ApiDocDirective.packageDotsToSlash(url.getPath, packageNameStyle)) + ".html"
334363
(baseUrl / path).withFragment(url.getFragment)
335364
}
336365

@@ -347,6 +376,7 @@ object JavadocDirective {
347376
// and including 8 starts with 1., so that's an easy way to tell if it's 9+ or not.
348377
val jdkDependentLinkStyle: LinkStyle = if (sys.props.get("java.specification.version").exists(_.startsWith("1."))) LinkStyleFrames else LinkStyleDirect
349378

379+
<<<<<<< HEAD
350380
final val JavadocLinkStyleProperty: Regex = raw"""javadoc\.(.*).link_style""".r
351381

352382
private[markdown] def url(link: String, baseUrl: Url, linkStyle: LinkStyle): Url = {
@@ -358,6 +388,9 @@ object JavadocDirective {
358388
}
359389
}
360390

391+
final val JavadocLinkStyleProperty = raw"""javadoc\.(.*).link_style""".r
392+
final val JavadocPackageNameStyleProperty = raw"""javadoc\.(.*)\.package_name_style""".r
393+
361394
}
362395

363396
case class JavadocDirective(ctx: Writer.Context)
@@ -372,14 +405,31 @@ case class JavadocDirective(ctx: Writer.Context)
372405
case (property @ JavadocLinkStyleProperty(pkg), _) => (pkg, variables(property))
373406
}
374407

408+
val defaultPackageNameStyle = variables.getOrElse("javadoc.package_name_style", "startWithLowercase")
409+
val packagePackageNameStyle: Map[String, String] = variables.collect {
410+
case (property @ JavadocPackageNameStyleProperty(pkg), url) => (pkg, variables(property))
411+
}
412+
375413
override protected def resolveApiLink(link: String): Url = {
376414
val levels = link.split("[.]")
377415
val packages = (1 to levels.init.length).map(levels.take(_).mkString("."))
378416
val packagesDeepestFirst = packages.reverse
379417
val baseUrl = packagesDeepestFirst.collectFirst(baseUrls).getOrElse(defaultBaseUrl).resolve()
380418
val linkStyle = packagesDeepestFirst.collectFirst(packageLinkStyle).getOrElse(rootLinkStyle)
381-
url(link, baseUrl, linkStyle)
419+
val packageNameStyle = packagesDeepestFirst.collectFirst(packagePackageNameStyle).getOrElse(defaultPackageNameStyle)
420+
url(link, baseUrl, linkStyle, packageNameStyle)
421+
422+
}
423+
424+
private def dollarDollarToClassDot(s: String) = s.replaceAll("\\$\\$", ".")
382425

426+
private[markdown] def url(link: String, baseUrl: Url, linkStyle: LinkStyle, packageNameStyle: String): Url = {
427+
val url = Url(link).base
428+
val path = dollarDollarToClassDot(ApiDocDirective.packageDotsToSlash(url.getPath, packageNameStyle)) + ".html"
429+
linkStyle match {
430+
case LinkStyleFrames => baseUrl.withEndingSlash.withQuery(path).withFragment(url.getFragment)
431+
case LinkStyleDirect => (baseUrl / path).withFragment(url.getFragment)
432+
}
383433
}
384434
}
385435

docs/src/main/paradox/directives/linking.md

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,12 +83,46 @@ URL.
8383

8484
The `@scaladoc` directive also supports site root relative base URLs using the `.../` syntax.
8585

86+
The directive will identify inner classes and resolve a reference like
87+
`@scaladoc[Consumer.Control](akka.kafka.scaladsl.Consumer.Control)` to
88+
<https://doc.akka.io/api/alpakka-kafka/current/akka/kafka/scaladsl/Consumer$$Control.html>.
89+
This is working fine as long as all (sub)package names are starting with a lowercase
90+
character while class names start with an uppercase character -- which is most often
91+
the case.
92+
93+
In a situation where a (sub)package name starts with an uppercase character the
94+
reference is resolved incorrectly. This can be fixed by configuring the properties
95+
`scaladoc.<package-prefix>.package_name_style` or the default
96+
`scaladoc.package_name_style` and set it to `startWithAnycase`.
97+
The directive will match the link text with the longest common package prefix
98+
and use the default style as a fall-back if nothing else matches. Keep in mind
99+
that the `OuterClass.InnerClass` notation is no longer working then and has
100+
to be replaced by `OuterClass$$InnerClass`.
101+
102+
For example, given:
103+
104+
```sbt
105+
paradoxProperties in Compile ++= Map(
106+
//...
107+
"scaladoc.com.example.package_name_style" -> s"startWithAnycase"
108+
)
109+
```
110+
111+
```markdown
112+
@scaladoc[SomeClass](com.example.Some.Library.SomeClass)
113+
@scaladoc[Outer.Inner](com.example.Some.Library.Outer$$Inner)
114+
@scaladoc[Consumer.Control](akka.kafka.scaladsl.Consumer.Control)
115+
```
116+
117+
Then all are being resolved to the correct URL.
118+
86119
@@@ Note
87120

88121
The [sbt-paradox-apidoc](https://github.com/lightbend/sbt-paradox-apidoc) plugin creates `@scaladoc` and `@javadoc` API links by searching the class paths for the appropriate class to link to.
89122

90123
@@@
91124

125+
92126
#### @javadoc directive
93127

94128
Use the `@javadoc` directives to link to Javadoc sites based on the package
@@ -120,6 +154,42 @@ associated with the `java.specification.version` system property.
120154

121155
The `@javadoc` directive also supports site root relative base URLs using the `.../` syntax.
122156

157+
The directive will identify inner classes and resolve a reference like
158+
`@javadoc[Flow.Subscriber](java.util.concurrent.Flow.Subscriber)` to
159+
<https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/concurrent/Flow.Subscriber.html>.
160+
This is working fine as long as all (sub)package names are starting with a lowercase
161+
character while class names start with an uppercase character -- which is most often
162+
the case.
163+
164+
In a situation where a (sub)package name starts with an uppercase character the
165+
reference is resolved incorrectly. This can be fixed by configuring the properties
166+
`javadoc.<package-prefix>.package_name_style` or the default
167+
`javadoc.package_name_style` and set it to `startWithAnycase`.
168+
The directive will match the link text with the longest common package prefix
169+
and use the default style as a fall-back if nothing else matches. Keep in mind
170+
that the `OuterClass.InnerClass` notation is no longer working then. In this case
171+
the class has to be referenced as `OuterClass$$InnerClass` which is being resolved
172+
back to the `.`-notation.
173+
174+
For example, given:
175+
176+
```sbt
177+
paradoxProperties in Compile ++= Map(
178+
//...
179+
"javadoc.com.example.package_name_style" -> s"startWithAnycase"
180+
)
181+
```
182+
183+
```markdown
184+
@javadoc[SomeClass](com.example.Some.Library.SomeClass)
185+
@javadoc[Outer.Inner](com.example.Some.Library.Outer$$Inner)
186+
@javadoc[outer.Inner](com.example.Some.Library.outer$$Inner)
187+
@javadoc[Outer.inner](com.example.Some.Library.Outer$$inner)
188+
@javadoc[Consumer.Control](akka.kafka.scaladsl.Consumer.Control)
189+
```
190+
191+
Then all are being resolved to the correct URL.
192+
123193
@@@ Note
124194

125195
The [sbt-paradox-apidoc](https://github.com/lightbend/sbt-paradox-apidoc) plugin creates `@scaladoc` and `@javadoc` API links by searching the class paths for the appropriate class to link to.

tests/src/test/scala/com/lightbend/paradox/markdown/JavadocDirectiveSpec.scala

Lines changed: 66 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,23 +20,53 @@ import com.lightbend.paradox.ParadoxException
2020

2121
class JavadocDirectiveSpec extends MarkdownBaseSpec {
2222

23-
import JavadocDirective._
24-
2523
implicit val context = writerContextWithProperties(
2624
"javadoc.base_url" -> "http://www.reactive-streams.org/reactive-streams-1.0.0-javadoc/",
2725
"javadoc.link_style" -> "frames",
2826
"javadoc.java.base_url" -> "https://docs.oracle.com/javase/8/docs/api/",
2927
"javadoc.akka.base_url" -> "http://doc.akka.io/japi/akka/2.4.10",
3028
"javadoc.akka.http.base_url" -> "http://doc.akka.io/japi/akka-http/10.0.0/index.html",
3129
"javadoc.root.relative.base_url" -> ".../javadoc/api/",
32-
"javadoc.broken.base_url" -> "https://c|"
30+
"javadoc.broken.base_url" -> "https://c|",
31+
"javadoc.org.example.base_url" -> "http://example.org/api/0.1.2/"
3332
)
3433

34+
def renderedMd(url: String, title: String, name: String, prefix: String = "", suffix: String = "") =
35+
html(Seq(prefix, """<p><a href="""", url, """" title="""", title, """"><code>""", name, """</code></a></p>""", suffix).mkString(""))
36+
3537
"javadoc directive" should "create links using configured URL templates" in {
3638
markdown("@javadoc[Publisher](org.reactivestreams.Publisher)") shouldEqual
3739
html("""<p><a href="http://www.reactive-streams.org/reactive-streams-1.0.0-javadoc/?org/reactivestreams/Publisher.html" title="org.reactivestreams.Publisher"><code>Publisher</code></a></p>""")
3840
}
3941

42+
it should "create accept digits in package names" in {
43+
markdown("@javadoc[ObjectMetadata](akka.s3.ObjectMetadata)") shouldEqual
44+
renderedMd("http://doc.akka.io/japi/akka/2.4.10/?akka/s3/ObjectMetadata.html", "akka.s3.ObjectMetadata", "ObjectMetadata")
45+
}
46+
47+
it should "create accept also non ascii characters (java letters) in package names" in {
48+
markdown("@javadoc[S0meTHing](org.example.some.stränµè.ıãß.S0meTHing)") shouldEqual
49+
renderedMd("http://example.org/api/0.1.2/?org/example/some/stränµè/ıãß/S0meTHing.html", "org.example.some.stränµè.ıãß.S0meTHing", "S0meTHing")
50+
}
51+
52+
it should "create accept also non ascii characters (java letters) in class names" in {
53+
markdown("@javadoc[Grüße](org.example.some.Grüße)") shouldEqual
54+
renderedMd("http://example.org/api/0.1.2/?org/example/some/Grüße.html", "org.example.some.Grüße", "Grüße")
55+
}
56+
57+
it should "create accept uppercase in package names" in {
58+
markdown("@javadoc[S0meTHing](org.example.soME.stränµè.ıãß.S0meTHing)") shouldEqual
59+
renderedMd("http://example.org/api/0.1.2/?org/example/soME/stränµè/ıãß/S0meTHing.html", "org.example.soME.stränµè.ıãß.S0meTHing", "S0meTHing")
60+
}
61+
62+
it should "create accept subpackages starting with uppercase" in {
63+
implicit val context = writerContextWithProperties(
64+
"javadoc.package_name_style" -> "startWithAnycase",
65+
"javadoc.org.example.base_url" -> "http://example.org/api/0.1.2/")
66+
markdown("@javadoc[S0meTHing](org.example.soME.stränµè.ıãß.你好.S0meTHing)") shouldEqual
67+
renderedMd("http://example.org/api/0.1.2/?org/example/soME/stränµè/ıãß/你好/S0meTHing.html", "org.example.soME.stränµè.ıãß.你好.S0meTHing", "S0meTHing")
68+
}
69+
4070
it should "support 'javadoc:' as an alternative name" in {
4171
markdown("@javadoc:[Publisher](org.reactivestreams.Publisher)") shouldEqual
4272
html("""<p><a href="http://www.reactive-streams.org/reactive-streams-1.0.0-javadoc/?org/reactivestreams/Publisher.html" title="org.reactivestreams.Publisher"><code>Publisher</code></a></p>""")
@@ -118,19 +148,42 @@ class JavadocDirectiveSpec extends MarkdownBaseSpec {
118148
}
119149

120150
it should "correctly link to an inner JRE class" in {
121-
url(
122-
"java.util.concurrent.Flow.Subscriber",
123-
Url("https://docs.oracle.com/en/java/javase/11/docs/api/java.base/"),
124-
LinkStyleDirect
125-
) should be(Url("https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/concurrent/Flow.Subscriber.html"))
151+
val ctx = context.andThen(c => c.copy(properties = c.properties
152+
.updated("javadoc.java.link_style", "direct")
153+
.updated("javadoc.java.base_url", "https://docs.oracle.com/en/java/javase/11/docs/api/java.base/")
154+
))
155+
markdown("@javadoc:[Flow.Subscriber](java.util.concurrent.Flow.Subscriber)")(ctx) shouldEqual
156+
html("""<p><a href="https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/concurrent/Flow.Subscriber.html" title="java.util.concurrent.Flow.Subscriber"><code>Flow.Subscriber</code></a></p>""")
126157
}
127158

128159
it should "correctly link to an inner Akka class" in {
129-
url(
130-
"akka.actor.testkit.typed.Effect.MessageAdapter",
131-
Url("https://doc.akka.io/japi/akka/current/"),
132-
LinkStyleDirect
133-
) should be(Url("https://doc.akka.io/japi/akka/current/akka/actor/testkit/typed/Effect.MessageAdapter.html"))
160+
val ctx = context.andThen(c => c.copy(properties = c.properties
161+
.updated("javadoc.akka.link_style", "direct")
162+
.updated("javadoc.akka.base_url", "https://doc.akka.io/japi/akka/current/")
163+
))
164+
markdown("@javadoc:[Effect.MessageAdapter](akka.actor.testkit.typed.Effect.MessageAdapter)")(ctx) shouldEqual
165+
html("""<p><a href="https://doc.akka.io/japi/akka/current/akka/actor/testkit/typed/Effect.MessageAdapter.html" title="akka.actor.testkit.typed.Effect.MessageAdapter"><code>Effect.MessageAdapter</code></a></p>""")
166+
}
167+
168+
it should "correctly link to an inner class if a subpackage starts with an uppercase character" in {
169+
val ctx = context.andThen(c => c.copy(properties = c.properties
170+
.updated("javadoc.org.example.package_name_style", "startWithAnycase")
171+
))
172+
markdown("@javadoc:[Outer.Inner](org.example.Lib.Outer$$Inner)")(ctx) shouldEqual
173+
renderedMd("http://example.org/api/0.1.2/?org/example/Lib/Outer.Inner.html", "org.example.Lib.Outer.Inner", "Outer.Inner")
174+
}
175+
176+
it should "correctly link to an inner class if the outer class starts with a lowercase character" in {
177+
markdown("@javadoc:[outer.Inner](org.example.lib.outer$$Inner)") shouldEqual
178+
renderedMd("http://example.org/api/0.1.2/?org/example/lib/outer.Inner.html", "org.example.lib.outer.Inner", "outer.Inner")
179+
}
180+
181+
it should "correctly link to an inner class if the inner class starts with a lowercase character" in {
182+
val ctx = context.andThen(c => c.copy(properties = c.properties
183+
.updated("javadoc.org.example.package_name_style", "startWithAnycase")
184+
))
185+
markdown("@javadoc:[Outer.inner](org.example.lib.Outer$$inner)")(ctx) shouldEqual
186+
renderedMd("http://example.org/api/0.1.2/?org/example/lib/Outer.inner.html", "org.example.lib.Outer.inner", "Outer.inner")
134187
}
135188

136189
}

0 commit comments

Comments
 (0)