Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

API doc directives: accept full character range in package names #431

Merged
merged 1 commit into from
Apr 7, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 53 additions & 16 deletions core/src/main/scala/com/lightbend/paradox/markdown/Directive.scala
Original file line number Diff line number Diff line change
Expand Up @@ -312,25 +312,53 @@ abstract class ApiDocDirective(name: String)
}

object ApiDocDirective {
/** This relies on the naming convention of packages being all-ascii-lowercase (which is rarely broken), numbers and underscore. */
def packageDotsToSlash(s: String): String = s.replaceAll("(\\b[a-z][a-z0-9_]*)\\.", "$1/")
/**
* Converts package dot notation to a path, separated by '/'
* Allow all valid java characters and java numbers to be used, according to the java lang spec.
*
* @param s package or full qualified class name to be converted.
* @param packageNameStyle Setting `startWithLowercase`` will get it wrong when a package name
* starts with an uppercase letter or when an inner class starts with
* a lowercase character, while `startWithAnycase` will derive the wrong
* path whenever an inner class is encountered.
* @return Resulting path.
*/
def packageDotsToSlash(s: String, packageNameStyle: String): String =
if (packageNameStyle == "startWithAnycase")
s.replaceAll("(\\b\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*)\\.", "$1/")
else
s.replaceAll("(\\b\\p{javaLowerCase}\\p{javaJavaIdentifierPart}*)\\.", "$1/")
}

object ScaladocDirective {
final val ScaladocPackageNameStyleProperty = raw"""scaladoc\.(.*)\.package_name_style""".r

}

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

import ScaladocDirective._

val defaultPackageNameStyle = variables.getOrElse("scaladoc.package_name_style", "startWithLowercase")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 to keep the default

val packagePackageNameStyle: Map[String, String] = variables.collect {
case (property @ ScaladocPackageNameStyleProperty(pkg), url) => (pkg, variables(property))
}

protected def resolveApiLink(link: String): Url = {
val levels = link.split("[.]")
val packages = (1 to levels.init.length).map(levels.take(_).mkString("."))
val baseUrl = packages.reverse.collectFirst(baseUrls).getOrElse(defaultBaseUrl).resolve()
url(link, baseUrl)
val packagesDeepestFirst = packages.reverse
val baseUrl = packagesDeepestFirst.collectFirst(baseUrls).getOrElse(defaultBaseUrl).resolve()
val packageNameStyle = packagesDeepestFirst.collectFirst(packagePackageNameStyle).getOrElse(defaultPackageNameStyle)
url(link, baseUrl, packageNameStyle)
}

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

private def url(link: String, baseUrl: Url): Url = {
private def url(link: String, baseUrl: Url, packageNameStyle: String): Url = {
val url = Url(link).base
val path = classDotsToDollarDollar(ApiDocDirective.packageDotsToSlash(url.getPath)) + ".html"
val path = classDotsToDollarDollar(ApiDocDirective.packageDotsToSlash(url.getPath, packageNameStyle)) + ".html"
(baseUrl / path).withFragment(url.getFragment)
}

Expand All @@ -348,15 +376,7 @@ object JavadocDirective {
val jdkDependentLinkStyle: LinkStyle = if (sys.props.get("java.specification.version").exists(_.startsWith("1."))) LinkStyleFrames else LinkStyleDirect

final val JavadocLinkStyleProperty: Regex = raw"""javadoc\.(.*).link_style""".r

private[markdown] def url(link: String, baseUrl: Url, linkStyle: LinkStyle): Url = {
val url = Url(link).base
val path = ApiDocDirective.packageDotsToSlash(url.getPath) + ".html"
linkStyle match {
case LinkStyleFrames => baseUrl.withEndingSlash.withQuery(path).withFragment(url.getFragment)
case LinkStyleDirect => (baseUrl / path).withFragment(url.getFragment)
}
}
final val JavadocPackageNameStyleProperty: Regex = raw"""javadoc\.(.*)\.package_name_style""".r

}

Expand All @@ -372,14 +392,31 @@ case class JavadocDirective(ctx: Writer.Context)
case (property @ JavadocLinkStyleProperty(pkg), _) => (pkg, variables(property))
}

val defaultPackageNameStyle = variables.getOrElse("javadoc.package_name_style", "startWithLowercase")
val packagePackageNameStyle: Map[String, String] = variables.collect {
case (property @ JavadocPackageNameStyleProperty(pkg), url) => (pkg, variables(property))
}

override protected def resolveApiLink(link: String): Url = {
val levels = link.split("[.]")
val packages = (1 to levels.init.length).map(levels.take(_).mkString("."))
val packagesDeepestFirst = packages.reverse
val baseUrl = packagesDeepestFirst.collectFirst(baseUrls).getOrElse(defaultBaseUrl).resolve()
val linkStyle = packagesDeepestFirst.collectFirst(packageLinkStyle).getOrElse(rootLinkStyle)
url(link, baseUrl, linkStyle)
val packageNameStyle = packagesDeepestFirst.collectFirst(packagePackageNameStyle).getOrElse(defaultPackageNameStyle)
url(link, baseUrl, linkStyle, packageNameStyle)

}

private def dollarDollarToClassDot(s: String) = s.replaceAll("\\$\\$", ".")

private[markdown] def url(link: String, baseUrl: Url, linkStyle: LinkStyle, packageNameStyle: String): Url = {
val url = Url(link).base
val path = dollarDollarToClassDot(ApiDocDirective.packageDotsToSlash(url.getPath, packageNameStyle)) + ".html"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So any names actually containing $$ are now broken - I suppose that should be exotic enough that this is OK.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes I also think that this might be a very theoretical case. If we want to get rid of it, we can also think of using a non java character, e.g. a .. (two dots) instead of $$. If I', right then this is invalid for any full qualified name.

linkStyle match {
case LinkStyleFrames => baseUrl.withEndingSlash.withQuery(path).withFragment(url.getFragment)
case LinkStyleDirect => (baseUrl / path).withFragment(url.getFragment)
}
}
}

Expand Down
70 changes: 70 additions & 0 deletions docs/src/main/paradox/directives/linking.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,12 +83,46 @@ URL.

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

The directive will identify inner classes and resolve a reference like
`@scaladoc[Consumer.Control](akka.kafka.scaladsl.Consumer.Control)` to
<https://doc.akka.io/api/alpakka-kafka/current/akka/kafka/scaladsl/Consumer$$Control.html>.
This is working fine as long as all (sub)package names are starting with a lowercase
character while class names start with an uppercase character -- which is most often
the case.

In a situation where a (sub)package name starts with an uppercase character the
reference is resolved incorrectly. This can be fixed by configuring the properties
`scaladoc.<package-prefix>.package_name_style` or the default
`scaladoc.package_name_style` and set it to `startWithAnycase`.
The directive will match the link text with the longest common package prefix
and use the default style as a fall-back if nothing else matches. Keep in mind
that the `OuterClass.InnerClass` notation is no longer working then and has
to be replaced by `OuterClass$$InnerClass`.

For example, given:

```sbt
paradoxProperties in Compile ++= Map(
//...
"scaladoc.com.example.package_name_style" -> s"startWithAnycase"
)
```

```markdown
@scaladoc[SomeClass](com.example.Some.Library.SomeClass)
@scaladoc[Outer.Inner](com.example.Some.Library.Outer$$Inner)
@scaladoc[Consumer.Control](akka.kafka.scaladsl.Consumer.Control)
```

Then all are being resolved to the correct URL.

@@@ Note

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.

@@@


#### @javadoc directive

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

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

The directive will identify inner classes and resolve a reference like
`@javadoc[Flow.Subscriber](java.util.concurrent.Flow.Subscriber)` to
<https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/concurrent/Flow.Subscriber.html>.
This is working fine as long as all (sub)package names are starting with a lowercase
character while class names start with an uppercase character -- which is most often
the case.

In a situation where a (sub)package name starts with an uppercase character the
reference is resolved incorrectly. This can be fixed by configuring the properties
`javadoc.<package-prefix>.package_name_style` or the default
`javadoc.package_name_style` and set it to `startWithAnycase`.
The directive will match the link text with the longest common package prefix
and use the default style as a fall-back if nothing else matches. Keep in mind
that the `OuterClass.InnerClass` notation is no longer working then. In this case
the class has to be referenced as `OuterClass$$InnerClass` which is being resolved
back to the `.`-notation.

For example, given:

```sbt
paradoxProperties in Compile ++= Map(
//...
"javadoc.com.example.package_name_style" -> s"startWithAnycase"
)
```

```markdown
@javadoc[SomeClass](com.example.Some.Library.SomeClass)
@javadoc[Outer.Inner](com.example.Some.Library.Outer$$Inner)
@javadoc[outer.Inner](com.example.Some.Library.outer$$Inner)
@javadoc[Outer.inner](com.example.Some.Library.Outer$$inner)
@javadoc[Consumer.Control](akka.kafka.scaladsl.Consumer.Control)
```

Then all are being resolved to the correct URL.

@@@ Note

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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,23 +20,53 @@ import com.lightbend.paradox.ParadoxException

class JavadocDirectiveSpec extends MarkdownBaseSpec {

import JavadocDirective._

implicit val context = writerContextWithProperties(
"javadoc.base_url" -> "http://www.reactive-streams.org/reactive-streams-1.0.0-javadoc/",
"javadoc.link_style" -> "frames",
"javadoc.java.base_url" -> "https://docs.oracle.com/javase/8/docs/api/",
"javadoc.akka.base_url" -> "http://doc.akka.io/japi/akka/2.4.10",
"javadoc.akka.http.base_url" -> "http://doc.akka.io/japi/akka-http/10.0.0/index.html",
"javadoc.root.relative.base_url" -> ".../javadoc/api/",
"javadoc.broken.base_url" -> "https://c|"
"javadoc.broken.base_url" -> "https://c|",
"javadoc.org.example.base_url" -> "http://example.org/api/0.1.2/"
)

def renderedMd(url: String, title: String, name: String, prefix: String = "", suffix: String = "") =
html(Seq(prefix, """<p><a href="""", url, """" title="""", title, """"><code>""", name, """</code></a></p>""", suffix).mkString(""))

"javadoc directive" should "create links using configured URL templates" in {
markdown("@javadoc[Publisher](org.reactivestreams.Publisher)") shouldEqual
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>""")
}

it should "create accept digits in package names" in {
markdown("@javadoc[ObjectMetadata](akka.s3.ObjectMetadata)") shouldEqual
renderedMd("http://doc.akka.io/japi/akka/2.4.10/?akka/s3/ObjectMetadata.html", "akka.s3.ObjectMetadata", "ObjectMetadata")
}

it should "create accept also non ascii characters (java letters) in package names" in {
markdown("@javadoc[S0meTHing](org.example.some.stränµè.ıãß.S0meTHing)") shouldEqual
renderedMd("http://example.org/api/0.1.2/?org/example/some/stränµè/ıãß/S0meTHing.html", "org.example.some.stränµè.ıãß.S0meTHing", "S0meTHing")
}

it should "create accept also non ascii characters (java letters) in class names" in {
markdown("@javadoc[Grüße](org.example.some.Grüße)") shouldEqual
renderedMd("http://example.org/api/0.1.2/?org/example/some/Grüße.html", "org.example.some.Grüße", "Grüße")
}

it should "create accept uppercase in package names" in {
markdown("@javadoc[S0meTHing](org.example.soME.stränµè.ıãß.S0meTHing)") shouldEqual
renderedMd("http://example.org/api/0.1.2/?org/example/soME/stränµè/ıãß/S0meTHing.html", "org.example.soME.stränµè.ıãß.S0meTHing", "S0meTHing")
}

it should "create accept subpackages starting with uppercase" in {
implicit val context = writerContextWithProperties(
"javadoc.package_name_style" -> "startWithAnycase",
"javadoc.org.example.base_url" -> "http://example.org/api/0.1.2/")
markdown("@javadoc[S0meTHing](org.example.soME.stränµè.ıãß.你好.S0meTHing)") shouldEqual
renderedMd("http://example.org/api/0.1.2/?org/example/soME/stränµè/ıãß/你好/S0meTHing.html", "org.example.soME.stränµè.ıãß.你好.S0meTHing", "S0meTHing")
}

it should "support 'javadoc:' as an alternative name" in {
markdown("@javadoc:[Publisher](org.reactivestreams.Publisher)") shouldEqual
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>""")
Expand Down Expand Up @@ -118,19 +148,42 @@ class JavadocDirectiveSpec extends MarkdownBaseSpec {
}

it should "correctly link to an inner JRE class" in {
url(
"java.util.concurrent.Flow.Subscriber",
Url("https://docs.oracle.com/en/java/javase/11/docs/api/java.base/"),
LinkStyleDirect
) should be(Url("https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/concurrent/Flow.Subscriber.html"))
val ctx = context.andThen(c => c.copy(properties = c.properties
.updated("javadoc.java.link_style", "direct")
.updated("javadoc.java.base_url", "https://docs.oracle.com/en/java/javase/11/docs/api/java.base/")
))
markdown("@javadoc:[Flow.Subscriber](java.util.concurrent.Flow.Subscriber)")(ctx) shouldEqual
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>""")
}

it should "correctly link to an inner Akka class" in {
url(
"akka.actor.testkit.typed.Effect.MessageAdapter",
Url("https://doc.akka.io/japi/akka/current/"),
LinkStyleDirect
) should be(Url("https://doc.akka.io/japi/akka/current/akka/actor/testkit/typed/Effect.MessageAdapter.html"))
val ctx = context.andThen(c => c.copy(properties = c.properties
.updated("javadoc.akka.link_style", "direct")
.updated("javadoc.akka.base_url", "https://doc.akka.io/japi/akka/current/")
))
markdown("@javadoc:[Effect.MessageAdapter](akka.actor.testkit.typed.Effect.MessageAdapter)")(ctx) shouldEqual
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>""")
}

it should "correctly link to an inner class if a subpackage starts with an uppercase character" in {
val ctx = context.andThen(c => c.copy(properties = c.properties
.updated("javadoc.org.example.package_name_style", "startWithAnycase")
))
markdown("@javadoc:[Outer.Inner](org.example.Lib.Outer$$Inner)")(ctx) shouldEqual
renderedMd("http://example.org/api/0.1.2/?org/example/Lib/Outer.Inner.html", "org.example.Lib.Outer.Inner", "Outer.Inner")
}

it should "correctly link to an inner class if the outer class starts with a lowercase character" in {
markdown("@javadoc:[outer.Inner](org.example.lib.outer$$Inner)") shouldEqual
renderedMd("http://example.org/api/0.1.2/?org/example/lib/outer.Inner.html", "org.example.lib.outer.Inner", "outer.Inner")
}

it should "correctly link to an inner class if the inner class starts with a lowercase character" in {
val ctx = context.andThen(c => c.copy(properties = c.properties
.updated("javadoc.org.example.package_name_style", "startWithAnycase")
))
markdown("@javadoc:[Outer.inner](org.example.lib.Outer$$inner)")(ctx) shouldEqual
renderedMd("http://example.org/api/0.1.2/?org/example/lib/Outer.inner.html", "org.example.lib.Outer.inner", "Outer.inner")
}

}
Loading