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

Provide url to Github origin markdown files, resolve #68

Closed
wants to merge 8 commits into from
Closed
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
34 changes: 29 additions & 5 deletions core/src/main/scala/com/lightbend/paradox/markdown/Directive.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package com.lightbend.paradox.markdown

import com.lightbend.paradox.tree.Tree.Location
import java.io.{ File, FileNotFoundException }
import java.nio.file.{ Path => jPath }

import org.pegdown.ast._
import org.pegdown.ast.DirectiveNode.Format._
Expand Down Expand Up @@ -144,15 +145,16 @@ abstract class ExternalLinkDirective(names: String*) extends InlineDirective(nam

import ExternalLinkDirective._

def resolveLink(location: String): Url
def resolveLink(location: String, relativePath: jPath): Url

def render(node: DirectiveNode, visitor: Visitor, printer: Printer): Unit =
new ExpLinkNode("", resolvedSource(node, page), node.contentsNode).accept(visitor)

override protected def resolvedSource(node: DirectiveNode, page: Page): String = {
val link = super.resolvedSource(node, page)
val relativePath = new File(".").getCanonicalFile.toPath.relativize(page.file.getCanonicalFile.toPath)
try {
resolveLink(link).base.normalize.toString
resolveLink(link, relativePath).base.normalize.toString
} catch {
case Url.Error(reason) =>
throw new LinkException(s"Failed to resolve [$link] referenced from [${page.path}] because $reason")
Expand All @@ -177,7 +179,7 @@ object ExternalLinkDirective {
case class ExtRefDirective(page: Page, variables: Map[String, String])
extends ExternalLinkDirective("extref", "extref:") with SourceDirective {

def resolveLink(link: String): Url = {
def resolveLink(link: String, relativePath: jPath): Url = {
link.split(":", 2) match {
case Array(scheme, expr) => PropertyUrl(s"extref.$scheme.base_url", variables.get).format(expr)
case _ => throw Url.Error("URL has no scheme")
Expand Down Expand Up @@ -209,7 +211,7 @@ abstract class ApiDocDirective(name: String, page: Page, variables: Map[String,
case (property @ ApiDocProperty(pkg), url) => (pkg, PropertyUrl(property, variables.get))
}

def resolveLink(link: String): Url = {
def resolveLink(link: String, relativePath: jPath): Url = {
val levels = link.split("[.]")
val packages = (1 to levels.init.size).map(levels.take(_).mkString("."))
val baseUrl = packages.reverse.collectFirst(baseUrls).getOrElse(defaultBaseUrl)
Expand Down Expand Up @@ -262,7 +264,7 @@ case class GitHubDirective(page: Page, variables: Map[String, String])

val baseUrl = PropertyUrl("github.base_url", variables.get)

def resolveLink(link: String): Url = {
def resolveLink(link: String, relativePath: jPath): Url = {
link match {
case IssuesLink(project, issue) => resolveProject(project) / "issues" / issue
case CommitLink(_, project, commit) => resolveProject(project) / "commit" / commit
Expand Down Expand Up @@ -290,6 +292,28 @@ case class GitHubDirective(page: Page, variables: Map[String, String])

}

/**
* Source directive
*
* Link to corresponding source file on Github if it exists.
* Uses Github directive to generate the directory corresponding to paradox.
*/
case class SourceMarkdownDirective(page: Page, variables: Map[String, String])
extends ExternalLinkDirective("source", "source:") with SourceDirective {

val TreeUrl = """(.*github.com/[^/]+/[^/]+/tree/[^/]+)""".r
val ProjectUrl = """(.*github.com/[^/]+/[^/]+).*""".r

val baseUrl = PropertyUrl("github.base_url", variables.get)
val paradoxDir = PropertyDirectory("github.paradox_dir", variables.get)

def resolveLink(link: String, relativePath: jPath): Url = baseUrl.collect {
case TreeUrl(url) => url + paradoxDir.normalize(link, relativePath)
case ProjectUrl(url) => url + "/tree/master" + paradoxDir.normalize(link, relativePath)
case _ => throw Url.Error("[github.base_url] is not a project or versioned tree URL")
}
}

/**
* Snip directive.
*
Expand Down
69 changes: 65 additions & 4 deletions core/src/main/scala/com/lightbend/paradox/markdown/Url.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@

package com.lightbend.paradox.markdown

import java.net.{ URI, URISyntaxException }
import java.net.{ URI, URISyntaxException, URLEncoder }
import java.nio.file.{ Path => jPath }
import java.io.File

/**
* Small wrapper around URI to help update individual components.
Expand All @@ -26,13 +28,14 @@ case class Url(base: URI) {
case path if path.endsWith("/index.html") => this
case path => copy(path = path + "/")
}
def /(path: String): Url = copy(path = base.getPath + "/" + path)
def /(path: String): Url = copy(path = base.getPath + (if (path != "") "/" + path else ""))
def withQuery(query: String): Url = copy(query = query)
def withFragment(fragment: String): Url = copy(fragment = fragment)
def copy(path: String = base.getPath, query: String = base.getQuery, fragment: String = base.getFragment) = {
val uri = new URI(base.getScheme, base.getUserInfo, base.getHost, base.getPort, path, query, fragment)
Url(uri.normalize)
}
override def toString(): String = base.toString
}

object Url {
Expand All @@ -53,19 +56,77 @@ object Url {
}
}

case class PropertyUrl(property: String, variables: String => Option[String]) {
def base = variables(property) match {
class BasePropertyClass(property: String, variables: String => Option[String]) {
def base: String = variables(property) match {
case Some(baseUrl) => baseUrl
case None => throw Url.Error(s"property [$property] is not defined")
}

def resolve(): Url = {
Url.parse(base, s"property [$property] contains an invalid URL")
}
}

case class PropertyUrl(property: String, variables: String => Option[String]) extends BasePropertyClass(property, variables) {
def format(args: String*) = Url(base.format(args: _*))

def collect(f: PartialFunction[String, String]): Url = {
PropertyUrl(property, variables(_).collect(f)).resolve
}
}

case class PropertyDirectory(property: String, variables: String => Option[String]) extends BasePropertyClass(property, variables) {
override def base: String = variables(property) match {
case Some(baseUrl) => checkSeparatorDuplicates(normalizeBase(baseUrl))
case None => throw Url.Error(s"property [$property] is not defined")
}

def normalize(link: String, sourcePath: jPath): String = {
val additionalLink = convertLink(link, sourcePath)
Url.parse(convertToOsSeparator("/" + PropertyDirectory(property, variables).resolve.toString + "/src/main/paradox/" + additionalLink),
s"link [$additionalLink] contains an invalid URL").toString
}

private def convertToOsSeparator(path: String): String = {
if (File.separator == "\\")
new URI(URLEncoder.encode(path, "UTF-8")).getPath
else
path
}

private def convertLink(link: String, sourcePath: jPath, extensionExpected: String = ".md"): String = link match {
case "" => sourcePath.toString
case l if (!l.endsWith(extensionExpected)) => throw Url.Error(s"[$l] is not a markdown (.md) file")
case l => withoutLeaf(sourcePath.toString) + checkSeparatorDuplicates(normalizeLink(link))
}

private def withoutLeaf(path: String, separator: String = "/"): String = {
path.split(separator).reverse.tail.reverse.mkString(separator) match {
case "" => ""
case p => p + "/"
}
}

private def checkSeparatorDuplicates(normalizedPath: String, separator: String = "/"): String = {
normalizedPath.split(separator).contains("") match {
case true => throw Url.Error(s"[$normalizedPath] contains duplicate '/' separators")
case false => normalizedPath
}
}

private def normalizeBase(baseUrl: String): String = {
(baseUrl.startsWith("/"), baseUrl.endsWith("/")) match {
case (true, true) => baseUrl.drop(1).dropRight(1)
case (true, false) => baseUrl.drop(1)
case (false, true) => baseUrl.dropRight(1)
case _ => baseUrl
}
}

private def normalizeLink(link: String): String = {
link.startsWith("/") match {
case true => link.drop(1)
case false => link
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ object Writer {
ScaladocDirective(context.location.tree.label, context.properties),
JavadocDirective(context.location.tree.label, context.properties),
GitHubDirective(context.location.tree.label, context.properties),
SourceMarkdownDirective(context.location.tree.label, context.properties),
SnipDirective(context.location.tree.label, context.properties),
FiddleDirective(context.location.tree.label),
TocDirective(context.location),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ class FrontinSpec extends MarkdownBaseSpec {
f3.delete()
}

it should "return the corresponding properties at 'out' filed instantiation, but can return an empty String as the body" in {
it should "return the corresponding properties at 'out' field instantiation, but can return an empty String as the body" in {
Frontin(f4).header shouldEqual Map("out" -> "index.html")
Frontin(f4).body shouldEqual
prepare("""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ abstract class MarkdownBaseSpec extends FlatSpec with Matchers {
val markdownReader = new Reader
val markdownWriter = new Writer

def markdown(text: String)(implicit context: Location[Page] => Writer.Context = writerContext): String = {
markdownPages("test.md" -> text).getOrElse("test.html", "")
def markdown(text: String, pagePath: String = "test.md")(implicit context: Location[Page] => Writer.Context = writerContext): String = {
markdownPages(pagePath -> text).getOrElse(pagePath.dropRight(".md".length) + ".html", "")
}

def markdownPages(mappings: (String, String)*)(implicit context: Location[Page] => Writer.Context = writerContext): Map[String, String] = {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* Copyright © 2015 - 2016 Lightbend, Inc. <http://www.lightbend.com>
*
* 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

class SourceMarkdownDirectiveSpec extends MarkdownBaseSpec {
implicit val context = writerContextWithProperties(
"github.base_url" -> "https://github.com/lightbend/paradox/tree/v0.2.1",
"github.paradox_dir" -> "docs/manual")

"SourceMarkdown Directive" should "create links with configured base URL and markdown directory on github" in {
markdown("@source[link]()") shouldEqual
html("""<p><a href="https://github.com/lightbend/paradox/tree/v0.2.1/docs/manual/src/main/paradox/test.md">link</a></p>""")
}

it should "support 'source:' as an alternative name" in {
markdown("@source:[link]()") shouldEqual
html("""<p><a href="https://github.com/lightbend/paradox/tree/v0.2.1/docs/manual/src/main/paradox/test.md">link</a></p>""")
}

it should "display markdown source url of the file specified in parameter" in {
markdown("@source[link](index.md)") shouldEqual
html("""<p><a href="https://github.com/lightbend/paradox/tree/v0.2.1/docs/manual/src/main/paradox/index.md">link</a></p>""")
}

it should "display correct url for 'in-directory' files" in {
markdown("@source[link](some/index.md)") shouldEqual
html("""<p><a href="https://github.com/lightbend/paradox/tree/v0.2.1/docs/manual/src/main/paradox/some/index.md">link</a></p>""")
}

it should "display correct url for relative path to other files" in {
markdown("@source[link](../test.md)", "some/directory/index.md") shouldEqual
html("""<p><a href="https://github.com/lightbend/paradox/tree/v0.2.1/docs/manual/src/main/paradox/some/test.md">link</a></p>""")
}

it should "throw an error if github.paradox_dir contains '/' duplicates" in {
val duplicateSeparatorsContext = writerContextWithProperties(
"github.base_url" -> "https://github.com/lightbend/paradox/tree/v0.2.1",
"github.paradox_dir" -> "docs//dir")

the[ExternalLinkDirective.LinkException] thrownBy {
markdown("@source[link]()")(duplicateSeparatorsContext)
} should have message "Failed to resolve [] referenced from [test.html] because [docs//dir] contains duplicate '/' separators"
}

it should "throw an error if the link contains '/' duplicates" in {
the[ExternalLinkDirective.LinkException] thrownBy {
markdown("@source[link](some//link.md)")
} should have message "Failed to resolve [some//link.md] referenced from [test.html] because [some//link.md] contains duplicate '/' separators"
}

it should "throw an error if the link does not correspond to a markdown file" in {
the[ExternalLinkDirective.LinkException] thrownBy {
markdown("@source[link](some/link.mdi)")
} should have message "Failed to resolve [some/link.mdi] referenced from [test.html] because [some/link.mdi] is not a markdown (.md) file"
}

it should "throw an error if the link can't be converted into URL" in {
the[ExternalLinkDirective.LinkException] thrownBy {
markdown("@source[link](some/dir|index.md)")
} should have message "Failed to resolve [some/dir|index.md] referenced from [test.html] because link [some/dir|index.md] contains an invalid URL [/docs/manual/src/main/paradox/some/dir|index.md]"
}
}
10 changes: 10 additions & 0 deletions docs/src/main/paradox/features/linking.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ directives are configured via base URLs defined in `paradoxProperties`:
```sbt
paradoxProperties in Compile ++= Map(
"github.base_url" -> s"https://github.com/lightbend/paradox/tree/${version.value}",
"github.paradox_dir" -> "docs",
"scaladoc.akka.base_url" -> s"http://doc.akka.io/api/${Dependencies.akkaVersion}",
"extref.rfc.base_url" -> "http://tools.ietf.org/html/rfc%s"
)
Expand Down Expand Up @@ -102,6 +103,15 @@ points to a GitHub project, it is used as the GitHub base URL.

[github-autolinking]: https://help.github.com/articles/autolinked-references-and-urls/

#### @source directive

Use the `@source` directive to link to corresponding markdown source files on GitHub.

As in the previous section, the `github.base_url` property must be configured as well than the `github.paradox_dir` property which indicates the directory containing the paradox files (not the markdown files!), which are `build.sbt` and `src/main/paradox` files.

To display the source link of a file on GitHub, use its path relative to the current file.
For example, if the current file is `some/dir/index.md`, we could display a source link of `some/other.md` by writing `@source[link](../other.md)`. To display the source link of the current file, either use the common way `@source[link](index.md)` or simply `@source[link]()`.

#### @extref directive

Use the `@extref` directive to link to pages using custom URL templates.
Expand Down