diff --git a/project/build.scala b/project/build.scala index d2f2d1a..9d2e16d 100644 --- a/project/build.scala +++ b/project/build.scala @@ -15,6 +15,7 @@ "gitbucket", file("."), settings = Defaults.defaultSettings ++ ScalatraPlugin.scalatraWithJRebel ++ Seq( + sourcesInBase := false, organization := Organization, name := Name, version := Version, @@ -43,7 +44,9 @@ "ch.qos.logback" % "logback-classic" % "1.0.13" % "runtime", "org.eclipse.jetty" % "jetty-webapp" % "8.1.8.v20121106" % "container;provided", "org.eclipse.jetty.orbit" % "javax.servlet" % "3.0.0.v201112011016" % "container;provided;test" artifacts Artifact("javax.servlet", "jar", "jar"), - "junit" % "junit" % "4.11" % "test" + "junit" % "junit" % "4.11" % "test", + "org.asciidoctor" % "asciidoctor-java-integration" % "0.1.4", + "net.sourceforge.htmlcleaner" % "htmlcleaner" % "2.7" ), EclipseKeys.withSource := true, javacOptions in compile ++= Seq("-target", "6", "-source", "6"), diff --git a/src/main/scala/app/RepositoryViewerController.scala b/src/main/scala/app/RepositoryViewerController.scala index 1f5d831..dda0e61 100644 --- a/src/main/scala/app/RepositoryViewerController.scala +++ b/src/main/scala/app/RepositoryViewerController.scala @@ -252,7 +252,7 @@ } - private val readmeFiles = Seq("readme.md", "readme.markdown") + private val readmeFiles = view.helpers.renderableSuffixes.map(suffix => s"readme${suffix}") ++ Seq("readme.txt", "readme") /** * Provides HTML of the file list. @@ -270,14 +270,16 @@ //val revisions = Seq(if(revstr.isEmpty) repository.repository.defaultBranch else revstr, repository.branchList.head) // get specified commit JGitUtil.getDefaultBranch(git, repository, revstr).map { case (objectId, revision) => - defining(JGitUtil.getRevCommitFromId(git, objectId)){ revCommit => - // get files + defining(JGitUtil.getRevCommitFromId(git, objectId)) { revCommit => + // get files val files = JGitUtil.getFileList(git, revision, path) + val parentPath = if (path == ".") Nil else path.split("/").toList // process README.md or README.markdown val readme = files.find { file => readmeFiles.contains(file.name.toLowerCase) }.map { file => - file -> StringUtil.convertFromByteArray(JGitUtil.getContentFromId( + val path = (file.name :: parentPath.reverse).reverse + path -> StringUtil.convertFromByteArray(JGitUtil.getContentFromId( Git.open(getRepositoryDir(repository.owner, repository.name)), file.id, true).get) } diff --git a/src/main/scala/view/Asciidoc.scala b/src/main/scala/view/Asciidoc.scala new file mode 100644 index 0000000..5c131c4 --- /dev/null +++ b/src/main/scala/view/Asciidoc.scala @@ -0,0 +1,62 @@ +package view + +import org.asciidoctor.Asciidoctor +import org.asciidoctor.AttributesBuilder +import org.asciidoctor.OptionsBuilder +import org.asciidoctor.SafeMode +import org.htmlcleaner.HtmlCleaner +import org.htmlcleaner.HtmlNode +import org.htmlcleaner.SimpleHtmlSerializer +import org.htmlcleaner.TagNode +import org.htmlcleaner.TagNodeVisitor + +object Asciidoc { + + private[this] lazy val asciidoctor = Asciidoctor.Factory.create() + + /** + * Converts Markdown of Wiki pages to HTML. + */ + def toHtml(filePath: List[String], asciidoc: String, branch: String, repository: service.RepositoryService.RepositoryInfo, + enableWikiLink: Boolean, enableRefsLink: Boolean)(implicit context: app.Context): String = { + + val options = OptionsBuilder.options() + options.safe(SafeMode.SECURE) + val attributes = AttributesBuilder.attributes() + attributes.showTitle(true) + options.attributes(attributes.get()) + val rendered = asciidoctor.render(asciidoc, options) + + val path = filePath.reverse.tail.reverse match { + case Nil => "" + case p => p.mkString("", "/", "/") + } + val relativeUrlPrefix = s"${helpers.url(repository)}/blob/${branch}/${path}" + prefixRelativeUrls(rendered, relativeUrlPrefix) + } + + private[this] val exceptionPrefixes = Seq("#", "/", "http://", "https://") + + def prefixRelativeUrls(html: String, urlPrefix: String): String = { + val cleaner = new HtmlCleaner() + val node = cleaner.clean(html) + node.traverse(new TagNodeVisitor() { + override def visit(tagNode: TagNode, htmlNode: HtmlNode): Boolean = { + htmlNode match { + case tag: TagNode if tag.getName == "a" => + Option(tag.getAttributeByName("href")) foreach { href => + if (exceptionPrefixes.forall(p => !href.startsWith(p))) { + tag.addAttribute("href", s"${urlPrefix}${href}") + } + } + case _ => + } + // continue traversal + true + } + }) + new SimpleHtmlSerializer(cleaner.getProperties()).getAsString(node) + } + +} + diff --git a/src/main/scala/view/helpers.scala b/src/main/scala/view/helpers.scala index 8dc2adf..feb5f21 100644 --- a/src/main/scala/view/helpers.scala +++ b/src/main/scala/view/helpers.scala @@ -27,6 +27,16 @@ def plural(count: Int, singular: String, plural: String = ""): String = if(count == 1) singular else if(plural.isEmpty) singular + "s" else plural + private[this] val renderersBySuffix: Seq[(String, (List[String], String, String, service.RepositoryService.RepositoryInfo, Boolean, Boolean, app.Context) => Html)] = + Seq( + ".md" -> ((filePath, fileContent, branch, repository, enableWikiLink, enableRefsLink, context) => markdown(fileContent, repository, enableWikiLink, enableRefsLink)(context)), + ".markdown" -> ((filePath, fileContent, branch, repository, enableWikiLink, enableRefsLink, context) => markdown(fileContent, repository, enableWikiLink, enableRefsLink)(context)), + ".adoc" -> ((filePath, fileContent, branch, repository, enableWikiLink, enableRefsLink, context) => asciidoc(filePath, fileContent, branch, repository, enableWikiLink, enableRefsLink)(context)), + ".asciidoc" -> ((filePath, fileContent, branch, repository, enableWikiLink, enableRefsLink, context) => asciidoc(filePath, fileContent, branch, repository, enableWikiLink, enableRefsLink)(context)) + ) + + def renderableSuffixes: Seq[String] = renderersBySuffix.map(_._1) + /** * Converts Markdown of Wiki pages to HTML. */ @@ -34,6 +44,25 @@ enableWikiLink: Boolean, enableRefsLink: Boolean)(implicit context: app.Context): Html = Html(Markdown.toHtml(value, repository, enableWikiLink, enableRefsLink)) + def renderMarkup(filePath: List[String], fileContent: String, branch: String, + repository: service.RepositoryService.RepositoryInfo, + enableWikiLink: Boolean, enableRefsLink: Boolean)(implicit context: app.Context): Html = { + + val fileNameLower = filePath.reverse.head.toLowerCase + renderersBySuffix.find { case (suffix, _) => fileNameLower.endsWith(suffix) } match { + case Some((_, handler)) => handler(filePath, fileContent, branch, repository, enableWikiLink, enableRefsLink, context) + case None => Html( + s"${ + fileContent.split("(\\r\\n)|\\n").map(xml.Utility.escape(_)).mkString("
") + }
" + ) + } + } + + def asciidoc(filePath: List[String], value: String, branch: String, repository: service.RepositoryService.RepositoryInfo, + enableWikiLink: Boolean, enableRefsLink: Boolean)(implicit context: app.Context): Html = + Html(Asciidoc.toHtml(filePath, value, branch, repository, enableWikiLink, enableRefsLink)) + /** * Returns <img> which displays the avatar icon for the given user name. * This method looks up Gravatar if avatar icon has not been configured in user settings. diff --git a/src/main/twirl/repo/blob.scala.html b/src/main/twirl/repo/blob.scala.html index 25a9910..acb193a 100644 --- a/src/main/twirl/repo/blob.scala.html +++ b/src/main/twirl/repo/blob.scala.html @@ -37,7 +37,13 @@ @if(content.viewType == "text"){ -
@content.content.get
+ @defining(pathList.reverse.head) { file => + @if(renderableSuffixes.find(suffix => file.toLowerCase.endsWith(suffix))) { + @renderMarkup(pathList, content.content.get, branch, repository, false, false) + } else { +
@content.content.get
+ } + } } @if(content.viewType == "image"){ diff --git a/src/main/twirl/repo/files.scala.html b/src/main/twirl/repo/files.scala.html index 6b4739c..db21928 100644 --- a/src/main/twirl/repo/files.scala.html +++ b/src/main/twirl/repo/files.scala.html @@ -3,7 +3,7 @@ pathList: List[String], latestCommit: util.JGitUtil.CommitInfo, files: List[util.JGitUtil.FileInfo], - readme: Option[(util.JGitUtil.FileInfo, String)])(implicit context: app.Context) + readme: Option[(List[String], String)])(implicit context: app.Context) @import context._ @import view.helpers._ @html.main(s"${repository.owner}/${repository.name}", Some(repository)) { @@ -85,10 +85,10 @@ - @readme.map { case(file, content) => + @readme.map { case(filePath, content) =>
-
@file.name
-
@markdown(content, repository, false, false)
+
@filePath.reverse.head
+
@renderMarkup(filePath, content, branch, repository, false, false)
} }