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("\"").printEncoded(printChildrenToString(imageNode)).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._
@@ -30,10 +30,9 @@ $('#preview').click(function(){ $('#preview-area').html(' Previewing...'); $.post('@url(repository)/_preview', { - content : $('#content').val(), - enableWikiLink : @enableWikiLink, - enableCommitLink : @enableCommitLink, - enableIssueLink : @enableIssueLink + content : $('#content').val(), + enableWikiLink : @enableWikiLink, + enableRefsLink : @enableRefsLink }, function(data){ $('#preview-area').html(data); prettyPrint(); diff --git a/src/main/twirl/issues/create.scala.html b/src/main/twirl/issues/create.scala.html index 68d8eff..fec4193 100644 --- a/src/main/twirl/issues/create.scala.html +++ b/src/main/twirl/issues/create.scala.html @@ -43,7 +43,7 @@

- @helper.html.preview(repository, "", false, true, true, "width: 600px; height: 200px;") + @helper.html.preview(repository, "", false, true, "width: 600px; height: 200px;")
diff --git a/src/main/twirl/issues/issue.scala.html b/src/main/twirl/issues/issue.scala.html index c36405b..a12eafe 100644 --- a/src/main/twirl/issues/issue.scala.html +++ b/src/main/twirl/issues/issue.scala.html @@ -64,7 +64,7 @@
- @markdown(issue.content getOrElse "No description given.", repository, false, true, true) + @markdown(issue.content getOrElse "No description given.", repository, false, true)
@@ -82,7 +82,7 @@
- @markdown(comment.content, repository, false, true, true) + @markdown(comment.content, repository, false, true)
@comment.action.map { action => @@ -102,7 +102,7 @@
@avatar(loginAccount.get.userName, 48)
- @helper.html.preview(repository, "", false, true, true, "width: 680px; height: 100px;") + @helper.html.preview(repository, "", false, true, "width: 680px; height: 100px;")
diff --git a/src/main/twirl/issues/milestones/list.scala.html b/src/main/twirl/issues/milestones/list.scala.html index 76b5c24..e9ddb6c 100644 --- a/src/main/twirl/issues/milestones/list.scala.html +++ b/src/main/twirl/issues/milestones/list.scala.html @@ -81,7 +81,7 @@
@if(milestone.description.isDefined){
- @markdown(milestone.description.get, repository, false, false, false) + @markdown(milestone.description.get, repository, false, false)
} diff --git a/src/main/twirl/repo/files.scala.html b/src/main/twirl/repo/files.scala.html index 1cb2434..541ccdf 100644 --- a/src/main/twirl/repo/files.scala.html +++ b/src/main/twirl/repo/files.scala.html @@ -27,8 +27,6 @@ @link(latestCommit.summary, repository) @if(latestCommit.description.isDefined){ ... - } - @if(latestCommit.description.isDefined){ } @@ -79,7 +77,7 @@ @readme.map { content =>
README.md
-
@markdown(content, repository, false, false, false)
+
@markdown(content, repository, false, false)
} } \ No newline at end of file diff --git a/src/main/twirl/wiki/edit.scala.html b/src/main/twirl/wiki/edit.scala.html index 3da2b6c..580c83b 100644 --- a/src/main/twirl/wiki/edit.scala.html +++ b/src/main/twirl/wiki/edit.scala.html @@ -23,7 +23,7 @@
- @helper.html.preview(repository, page.map(_.content).getOrElse(""), true, false, false, "width: 900px; height: 400px;", "") + @helper.html.preview(repository, page.map(_.content).getOrElse(""), true, false, "width: 900px; height: 400px;", "") diff --git a/src/main/twirl/wiki/page.scala.html b/src/main/twirl/wiki/page.scala.html index 102454d..21a8a62 100644 --- a/src/main/twirl/wiki/page.scala.html +++ b/src/main/twirl/wiki/page.scala.html @@ -22,7 +22,7 @@
- @markdown(page.content, repository, true, false, false) + @markdown(page.content, repository, true, false)
Last edited by @page.committer at @datetime(page.time)