diff --git a/src/main/scala/app/IssuesController.scala b/src/main/scala/app/IssuesController.scala
index e430bb2..307610d 100644
--- a/src/main/scala/app/IssuesController.scala
+++ b/src/main/scala/app/IssuesController.scala
@@ -158,7 +158,7 @@
org.json4s.jackson.Serialization.write(
Map("title" -> x.title,
"content" -> view.Markdown.toHtml(x.content getOrElse "No description given.",
- repository, false, true, true)
+ repository, false, true)
))
}
} else Unauthorized
@@ -175,7 +175,7 @@
contentType = formats("json")
org.json4s.jackson.Serialization.write(
Map("content" -> view.Markdown.toHtml(x.content,
- repository, false, true, true)
+ repository, false, true)
))
}
} else Unauthorized
diff --git a/src/main/scala/app/RepositoryViewerController.scala b/src/main/scala/app/RepositoryViewerController.scala
index a70c254..43cb126 100644
--- a/src/main/scala/app/RepositoryViewerController.scala
+++ b/src/main/scala/app/RepositoryViewerController.scala
@@ -27,8 +27,7 @@
contentType = "text/html"
view.helpers.markdown(params("content"), repository,
params("enableWikiLink").toBoolean,
- params("enableCommitLink").toBoolean,
- params("enableIssueLink").toBoolean)
+ params("enableRefsLink").toBoolean)
})
/**
diff --git a/src/main/scala/service/RequestCache.scala b/src/main/scala/service/RequestCache.scala
new file mode 100644
index 0000000..758c373
--- /dev/null
+++ b/src/main/scala/service/RequestCache.scala
@@ -0,0 +1,26 @@
+package service
+
+import model._
+
+/**
+ * This service is used for a view helper mainly.
+ *
+ * It may be called many times in one request, so each method stores
+ * its result into the cache which available during a request.
+ */
+trait RequestCache {
+
+ def getIssue(userName: String, repositoryName: String, issueId: String)(implicit context: app.Context): Option[Issue] = {
+ context.cache(s"issue.${userName}/${repositoryName}#${issueId}"){
+ new IssuesService {}.getIssue(userName, repositoryName, issueId)
+ }
+ }
+
+ def getAccountByUserName(userName: String)(implicit context: app.Context): Option[Account] = {
+ context.cache(s"account.${userName}"){
+ new AccountService {}.getAccountByUserName(userName)
+ }
+ }
+
+}
+
diff --git a/src/main/scala/util/Implicits.scala b/src/main/scala/util/Implicits.scala
index 1de9e24..c475136 100644
--- a/src/main/scala/util/Implicits.scala
+++ b/src/main/scala/util/Implicits.scala
@@ -1,7 +1,7 @@
package util
-import twirl.api.Html
import scala.slick.driver.H2Driver.simple._
+import scala.util.matching.Regex
/**
* Provides some usable implicit conversions.
@@ -30,4 +30,23 @@
def &&(c2: => Column[Boolean], guard: => Boolean): Column[Boolean] = if(guard) c1 && c2 else c1
}
+ implicit class RichString(value: String){
+ def replaceBy(regex: Regex)(replace: Regex.MatchData => Option[String]): String = {
+ val sb = new StringBuilder()
+ var i = 0
+ regex.findAllIn(value).matchData.foreach { m =>
+ sb.append(value.substring(i, m.start))
+ i = m.end
+ replace(m) match {
+ case Some(s) => sb.append(s)
+ case None => sb.append(m.matched)
+ }
+ }
+ if(i < value.length){
+ sb.append(value.substring(i))
+ }
+ sb.toString
+ }
+ }
+
}
\ No newline at end of file
diff --git a/src/main/scala/view/AvatarImageProvider.scala b/src/main/scala/view/AvatarImageProvider.scala
new file mode 100644
index 0000000..33ac699
--- /dev/null
+++ b/src/main/scala/view/AvatarImageProvider.scala
@@ -0,0 +1,26 @@
+package view
+
+import service.RequestCache
+import twirl.api.Html
+import util.StringUtil
+
+trait AvatarImageProvider { self: RequestCache =>
+
+ /**
+ * Returns <img> which displays the avatar icon.
+ * Looks up Gravatar if avatar icon has not been configured in user settings.
+ */
+ protected def getAvatarImageHtml(userName: String, size: Int, tooltip: Boolean = false)(implicit context: app.Context): Html = {
+ val src = getAccountByUserName(userName).collect { case account if(account.image.isEmpty) =>
+ s"""http://www.gravatar.com/avatar/${StringUtil.md5(account.mailAddress)}?s=${size}"""
+ } getOrElse {
+ s"""${context.path}/${userName}/_avatar"""
+ }
+ if(tooltip){
+ Html(s"""""")
+ } else {
+ Html(s"""
""")
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/scala/view/LinkConverter.scala b/src/main/scala/view/LinkConverter.scala
new file mode 100644
index 0000000..968f34f
--- /dev/null
+++ b/src/main/scala/view/LinkConverter.scala
@@ -0,0 +1,33 @@
+package view
+
+import service.RequestCache
+import util.Implicits.RichString
+
+trait LinkConverter { self: RequestCache =>
+
+ /**
+ * Converts issue id, username and commit id to link.
+ */
+ protected def convertRefsLinks(value: String, repository: service.RepositoryService.RepositoryInfo,
+ issueIdPrefix: String = "#")(implicit context: app.Context): String = {
+ value
+ // escape HTML tags
+ .replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """)
+ // convert issue id to link
+ .replaceBy(("(^|\\W)" + issueIdPrefix + "(\\d+)(\\W|$)").r){ m =>
+ if(getIssue(repository.owner, repository.name, m.group(2)).isDefined){
+ Some(s"""${m.group(1)}#${m.group(2)}${m.group(3)}""")
+ } else {
+ Some(s"""${m.group(1)}#${m.group(2)}${m.group(3)}""")
+ }
+ }
+ // convert @username to link
+ .replaceBy("(^|\\W)@([a-zA-Z0-9\\-_]+)(\\W|$)".r){ m =>
+ getAccountByUserName(m.group(2)).map { _ =>
+ s"""${m.group(1)}@${m.group(2)}${m.group(3)}"""
+ }
+ }
+ // convert commit id to link
+ .replaceAll("(^|\\W)([a-f0-9]{40})(\\W|$)", s"""$$1$$2$$3""")
+ }
+}
diff --git a/src/main/scala/view/Markdown.scala b/src/main/scala/view/Markdown.scala
index b90ba10..51f3375 100644
--- a/src/main/scala/view/Markdown.scala
+++ b/src/main/scala/view/Markdown.scala
@@ -1,11 +1,13 @@
package view
import util.StringUtil
+import util.Implicits._
import org.parboiled.common.StringUtils
import org.pegdown._
import org.pegdown.ast._
import org.pegdown.LinkRenderer.Rendering
import scala.collection.JavaConverters._
+import service.RequestCache
object Markdown {
@@ -13,12 +15,17 @@
* Converts Markdown of Wiki pages to HTML.
*/
def toHtml(markdown: String, repository: service.RepositoryService.RepositoryInfo,
- enableWikiLink: Boolean, enableCommitLink: Boolean, enableIssueLink: Boolean)(implicit context: app.Context): String = {
+ enableWikiLink: Boolean, enableRefsLink: Boolean)(implicit context: app.Context): String = {
+ // escape issue id
+ val source = if(enableRefsLink){
+ markdown.replaceAll("(^|\\W)#([0-9]+)(\\W|$)", "$1issue:$2$3")
+ } else markdown
+
val rootNode = new PegDownProcessor(
Extensions.AUTOLINKS | Extensions.WIKILINKS | Extensions.FENCED_CODE_BLOCKS | Extensions.TABLES
- ).parseMarkdown(markdown.toCharArray)
+ ).parseMarkdown(source.toCharArray)
- new GitBucketHtmlSerializer(markdown, context, repository, enableWikiLink, enableCommitLink, enableIssueLink).toHtml(rootNode)
+ new GitBucketHtmlSerializer(markdown, repository, enableWikiLink, enableRefsLink).toHtml(rootNode)
}
}
@@ -64,15 +71,13 @@
class GitBucketHtmlSerializer(
markdown: String,
- context: app.Context,
repository: service.RepositoryService.RepositoryInfo,
enableWikiLink: Boolean,
- enableCommitLink: Boolean,
- enableIssueLink: Boolean
- ) extends ToHtmlSerializer(
+ enableRefsLink: Boolean
+ )(implicit val context: app.Context) extends ToHtmlSerializer(
new GitBucketLinkRender(context, repository, enableWikiLink),
Map[String, VerbatimSerializer](VerbatimSerializer.DEFAULT -> new GitBucketVerbatimSerializer).asJava
- ) {
+ ) with LinkConverter with RequestCache {
override protected def printImageTag(imageNode: SuperNode, url: String): Unit =
printer.print("
")
@@ -100,10 +105,7 @@
override def visit(node: TextNode) {
// convert commit id and username to link.
- val text = if(enableCommitLink) node.getText
- .replaceAll("(^|\\W)([0-9a-f]{40})(\\W|$)", s"""$$1$$2$$3""")
- .replaceAll("(^|\\W)@([a-zA-Z0-9\\-_]+)(\\W|$)", s"""$$1@$$2$$3""")
- else node.getText
+ val text = if(enableRefsLink) convertRefsLinks(node.getText, repository, "issue:") else node.getText
if (abbreviations.isEmpty) {
printer.print(text)
@@ -112,15 +114,4 @@
}
}
- override def visit(node: HeaderNode) {
- val text = markdown.substring(node.getStartIndex, node.getEndIndex - 1).trim
- if(enableIssueLink && text.matches("#[\\d]+")){
- // convert issue id to link
- val issueId = text.substring(1).toInt
- printer.print(s"""#${issueId}""")
- } else {
- printTag(node, "h" + node.getLevel)
- }
- }
-
}
\ No newline at end of file
diff --git a/src/main/scala/view/helpers.scala b/src/main/scala/view/helpers.scala
index 74d06bf..fa276a2 100644
--- a/src/main/scala/view/helpers.scala
+++ b/src/main/scala/view/helpers.scala
@@ -3,12 +3,12 @@
import java.text.SimpleDateFormat
import twirl.api.Html
import util.StringUtil
-import service.AccountService
+import service.RequestCache
/**
* Provides helper methods for Twirl templates.
*/
-object helpers {
+object helpers extends AvatarImageProvider with LinkConverter with RequestCache {
/**
* Format java.util.Date to "yyyy-MM-dd HH:mm:ss".
@@ -31,10 +31,23 @@
* Converts Markdown of Wiki pages to HTML.
*/
def markdown(value: String, repository: service.RepositoryService.RepositoryInfo,
- enableWikiLink: Boolean, enableCommitLink: Boolean, enableIssueLink: Boolean)(implicit context: app.Context): Html = {
- Html(Markdown.toHtml(value, repository, enableWikiLink, enableCommitLink, enableIssueLink))
+ enableWikiLink: Boolean, enableRefsLink: Boolean)(implicit context: app.Context): Html = {
+ Html(Markdown.toHtml(value, repository, enableWikiLink, enableRefsLink))
}
+ /**
+ * Returns <img> which displays the avatar icon.
+ * Looks up Gravatar if avatar icon has not been configured in user settings.
+ */
+ def avatar(userName: String, size: Int, tooltip: Boolean = false)(implicit context: app.Context): Html =
+ getAvatarImageHtml(userName, size, tooltip)
+
+ /**
+ * Converts commit id, issue id and username to the link.
+ */
+ def link(value: String, repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context): Html =
+ Html(convertRefsLinks(value, repository))
+
def activityMessage(message: String)(implicit context: app.Context): Html =
Html(message
.replaceAll("\\[issue:([^\\s]+?)/([^\\s]+?)#((\\d+))\\]" , s"""$$1/$$2#$$3""")
@@ -66,38 +79,6 @@
def assets(implicit context: app.Context): String =
s"${context.path}/assets"
- /**
- * Converts issue id and commit id to link.
- */
- def link(value: String, repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context): Html =
- Html(value
- // escape HTML tags
- .replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """)
- // convert issue id to link
- .replaceAll("(^|\\W)#(\\d+)(\\W|$)", s"""$$1#$$2$$3""")
- // convert commit id to link
- .replaceAll("(^|\\W)([a-f0-9]{40})(\\W|$)", s"""$$1$$2$$3"""))
-
-
- /**
- * Returns <img> which displays the avatar icon.
- * Looks up Gravatar if avatar icon has not been configured in user settings.
- */
- def avatar(userName: String, size: Int, tooltip: Boolean = false)(implicit context: app.Context): Html = {
- val account = context.cache(s"account.${userName}"){
- new AccountService {}.getAccountByUserName(userName)
- }
- val src = account.collect { case account if(account.image.isEmpty) =>
- s"""http://www.gravatar.com/avatar/${StringUtil.md5(account.mailAddress)}?s=${size}"""
- } getOrElse {
- s"""${context.path}/${userName}/_avatar"""
- }
- if(tooltip){
- Html(s"""
""")
- } else {
- Html(s"""
""")
- }
- }
/**
* Implicit conversion to add mkHtml() to Seq[Html].
diff --git a/src/main/twirl/helper/preview.scala.html b/src/main/twirl/helper/preview.scala.html
index 642c3bc..9c1a607 100644
--- a/src/main/twirl/helper/preview.scala.html
+++ b/src/main/twirl/helper/preview.scala.html
@@ -1,5 +1,5 @@
-@(repository: service.RepositoryService.RepositoryInfo, content: String, enableWikiLink: Boolean,
- enableCommitLink: Boolean, enableIssueLink: Boolean, style: String = "", placeholder: String = "Leave a comment")(implicit context: app.Context)
+@(repository: service.RepositoryService.RepositoryInfo, content: String, enableWikiLink: Boolean, enableRefsLink: Boolean,
+ style: String = "", placeholder: String = "Leave a comment")(implicit context: app.Context)
@import context._
@import view.helpers._