Merge pull request #288 from lefou/asciidoctorj

Support AsciiDoc markup for all files + Plain text Readme's
This commit is contained in:
Naoki Takezoe
2014-04-04 13:09:45 +09:00
6 changed files with 112 additions and 10 deletions

View File

@@ -15,6 +15,7 @@ object MyBuild extends Build {
"gitbucket", "gitbucket",
file("."), file("."),
settings = Defaults.defaultSettings ++ ScalatraPlugin.scalatraWithJRebel ++ Seq( settings = Defaults.defaultSettings ++ ScalatraPlugin.scalatraWithJRebel ++ Seq(
sourcesInBase := false,
organization := Organization, organization := Organization,
name := Name, name := Name,
version := Version, version := Version,
@@ -43,7 +44,9 @@ object MyBuild extends Build {
"ch.qos.logback" % "logback-classic" % "1.0.13" % "runtime", "ch.qos.logback" % "logback-classic" % "1.0.13" % "runtime",
"org.eclipse.jetty" % "jetty-webapp" % "8.1.8.v20121106" % "container;provided", "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"), "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, EclipseKeys.withSource := true,
javacOptions in compile ++= Seq("-target", "6", "-source", "6"), javacOptions in compile ++= Seq("-target", "6", "-source", "6"),

View File

@@ -252,7 +252,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
} }
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. * Provides HTML of the file list.
@@ -273,11 +273,13 @@ trait RepositoryViewerControllerBase extends ControllerBase {
defining(JGitUtil.getRevCommitFromId(git, objectId)) { revCommit => defining(JGitUtil.getRevCommitFromId(git, objectId)) { revCommit =>
// get files // get files
val files = JGitUtil.getFileList(git, revision, path) val files = JGitUtil.getFileList(git, revision, path)
val parentPath = if (path == ".") Nil else path.split("/").toList
// process README.md or README.markdown // process README.md or README.markdown
val readme = files.find { file => val readme = files.find { file =>
readmeFiles.contains(file.name.toLowerCase) readmeFiles.contains(file.name.toLowerCase)
}.map { file => }.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) Git.open(getRepositoryDir(repository.owner, repository.name)), file.id, true).get)
} }

View File

@@ -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)
}
}

View File

@@ -27,6 +27,16 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
def plural(count: Int, singular: String, plural: String = ""): String = def plural(count: Int, singular: String, plural: String = ""): String =
if(count == 1) singular else if(plural.isEmpty) singular + "s" else plural 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. * Converts Markdown of Wiki pages to HTML.
*/ */
@@ -34,6 +44,25 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
enableWikiLink: Boolean, enableRefsLink: Boolean)(implicit context: app.Context): Html = enableWikiLink: Boolean, enableRefsLink: Boolean)(implicit context: app.Context): Html =
Html(Markdown.toHtml(value, repository, enableWikiLink, enableRefsLink)) 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"<tt>${
fileContent.split("(\\r\\n)|\\n").map(xml.Utility.escape(_)).mkString("<br/>")
}</tt>"
)
}
}
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 &lt;img&gt; which displays the avatar icon for the given user name. * Returns &lt;img&gt; 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. * This method looks up Gravatar if avatar icon has not been configured in user settings.

View File

@@ -37,8 +37,14 @@
<tr> <tr>
<td> <td>
@if(content.viewType == "text"){ @if(content.viewType == "text"){
@defining(pathList.reverse.head) { file =>
@if(renderableSuffixes.find(suffix => file.toLowerCase.endsWith(suffix))) {
@renderMarkup(pathList, content.content.get, branch, repository, false, false)
} else {
<pre class="prettyprint linenums blob">@content.content.get</pre> <pre class="prettyprint linenums blob">@content.content.get</pre>
} }
}
}
@if(content.viewType == "image"){ @if(content.viewType == "image"){
<img src="?raw=true"/> <img src="?raw=true"/>
} }

View File

@@ -3,7 +3,7 @@
pathList: List[String], pathList: List[String],
latestCommit: util.JGitUtil.CommitInfo, latestCommit: util.JGitUtil.CommitInfo,
files: List[util.JGitUtil.FileInfo], 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 context._
@import view.helpers._ @import view.helpers._
@html.main(s"${repository.owner}/${repository.name}", Some(repository)) { @html.main(s"${repository.owner}/${repository.name}", Some(repository)) {
@@ -85,10 +85,10 @@
</table> </table>
</div> </div>
@readme.map { case(file, content) => @readme.map { case(filePath, content) =>
<div id="readme" class="box"> <div id="readme" class="box">
<div class="box-header">@file.name</div> <div class="box-header">@filePath.reverse.head</div>
<div class="box-content markdown-body">@markdown(content, repository, false, false)</div> <div class="box-content markdown-body">@renderMarkup(filePath, content, branch, repository, false, false)</div>
</div> </div>
} }
} }