diff --git a/src/main/scala/app/IssuesController.scala b/src/main/scala/app/IssuesController.scala index b6b7bb9..d2bb683 100644 --- a/src/main/scala/app/IssuesController.scala +++ b/src/main/scala/app/IssuesController.scala @@ -195,7 +195,7 @@ org.json4s.jackson.Serialization.write( Map("title" -> x.title, "content" -> view.Markdown.toHtml(x.content getOrElse "No description given.", - repository, false, true) + repository, false, true, true, isEditable(x.userName, x.repositoryName, x.openedUserName)) )) } } else Unauthorized @@ -212,7 +212,7 @@ contentType = formats("json") org.json4s.jackson.Serialization.write( Map("content" -> view.Markdown.toHtml(x.content, - repository, false, true) + repository, false, true, true, isEditable(x.userName, x.repositoryName, x.commentedUserName)) )) } } else Unauthorized diff --git a/src/main/scala/app/RepositoryViewerController.scala b/src/main/scala/app/RepositoryViewerController.scala index 4291730..18a9f3f 100644 --- a/src/main/scala/app/RepositoryViewerController.scala +++ b/src/main/scala/app/RepositoryViewerController.scala @@ -77,7 +77,9 @@ contentType = "text/html" view.helpers.markdown(params("content"), repository, params("enableWikiLink").toBoolean, - params("enableRefsLink").toBoolean) + params("enableRefsLink").toBoolean, + params("enableTaskList").toBoolean, + hasWritePermission(repository.owner, repository.name, context.loginAccount)) }) /** diff --git a/src/main/scala/view/Markdown.scala b/src/main/scala/view/Markdown.scala index 0512024..08c3dd4 100644 --- a/src/main/scala/view/Markdown.scala +++ b/src/main/scala/view/Markdown.scala @@ -9,6 +9,7 @@ import org.pegdown.LinkRenderer.Rendering import java.text.Normalizer import java.util.Locale +import java.util.regex.Pattern import scala.collection.JavaConverters._ import service.{RequestCache, WikiService} @@ -18,17 +19,23 @@ * Converts Markdown of Wiki pages to HTML. */ def toHtml(markdown: String, repository: service.RepositoryService.RepositoryInfo, - enableWikiLink: Boolean, enableRefsLink: Boolean)(implicit context: app.Context): String = { + enableWikiLink: Boolean, enableRefsLink: Boolean, + enableTaskList: Boolean = false, hasWritePermission: Boolean = false)(implicit context: app.Context): String = { // escape issue id - val source = if(enableRefsLink){ + val s = if(enableRefsLink){ markdown.replaceAll("(?<=(\\W|^))#(\\d+)(?=(\\W|$))", "issue:$2") } else markdown + // escape task list + val source = if(enableTaskList){ + GitBucketHtmlSerializer.escapeTaskList(s) + } else s + val rootNode = new PegDownProcessor( Extensions.AUTOLINKS | Extensions.WIKILINKS | Extensions.FENCED_CODE_BLOCKS | Extensions.TABLES | Extensions.HARDWRAPS ).parseMarkdown(source.toCharArray) - new GitBucketHtmlSerializer(markdown, repository, enableWikiLink, enableRefsLink).toHtml(rootNode) + new GitBucketHtmlSerializer(markdown, repository, enableWikiLink, enableRefsLink, enableTaskList, hasWritePermission).toHtml(rootNode) } } @@ -82,7 +89,9 @@ markdown: String, repository: service.RepositoryService.RepositoryInfo, enableWikiLink: Boolean, - enableRefsLink: Boolean + enableRefsLink: Boolean, + enableTaskList: Boolean, + hasWritePermission: Boolean )(implicit val context: app.Context) extends ToHtmlSerializer( new GitBucketLinkRender(context, repository, enableWikiLink), Map[String, VerbatimSerializer](VerbatimSerializer.DEFAULT -> new GitBucketVerbatimSerializer).asJava @@ -143,7 +152,10 @@ override def visit(node: TextNode): Unit = { // convert commit id and username to link. - val text = if(enableRefsLink) convertRefsLinks(node.getText, repository, "issue:") else node.getText + val t = if(enableRefsLink) convertRefsLinks(node.getText, repository, "issue:") else node.getText + + // convert task list to checkbox. + val text = if(enableTaskList) GitBucketHtmlSerializer.convertCheckBox(t, hasWritePermission) else t if (abbreviations.isEmpty) { printer.print(text) @@ -151,6 +163,28 @@ printWithAbbreviations(text) } } + + override def visit(node: BulletListNode): Unit = { + if (printChildrenToString(node).contains("""class="task-list-item-checkbox" """)) { + printer.println().print("""") + } else { + printIndentedTag(node, "ul") + } + } + + override def visit(node: ListItemNode): Unit = { + if (printChildrenToString(node).contains("""class="task-list-item-checkbox" """)) { + printer.println() + printer.print("""
  • """) + visitChildren(node) + printer.print("
  • ") + } else { + printer.println() + printTag(node, "li") + } + } } object GitBucketHtmlSerializer { @@ -163,4 +197,14 @@ val noSpecialChars = StringUtil.urlEncode(normalized) noSpecialChars.toLowerCase(Locale.ENGLISH) } + + def escapeTaskList(text: String): String = { + Pattern.compile("""^( *)- \[([x| ])\] """, Pattern.MULTILINE).matcher(text).replaceAll("$1* task:$2: ") + } + + def convertCheckBox(text: String, hasWritePermission: Boolean): String = { + val disabled = if (hasWritePermission) "" else "disabled" + text.replaceAll("task:x:", """") + .replaceAll("task: :", """") + } } diff --git a/src/main/scala/view/helpers.scala b/src/main/scala/view/helpers.scala index 78f658f..7c11f1b 100644 --- a/src/main/scala/view/helpers.scala +++ b/src/main/scala/view/helpers.scala @@ -48,8 +48,8 @@ * Converts Markdown of Wiki pages to HTML. */ def markdown(value: String, repository: service.RepositoryService.RepositoryInfo, - enableWikiLink: Boolean, enableRefsLink: Boolean)(implicit context: app.Context): Html = - Html(Markdown.toHtml(value, repository, enableWikiLink, enableRefsLink)) + enableWikiLink: Boolean, enableRefsLink: Boolean, enableTaskList: Boolean = false, hasWritePermission: Boolean = false)(implicit context: app.Context): Html = + Html(Markdown.toHtml(value, repository, enableWikiLink, enableRefsLink, enableTaskList, hasWritePermission)) def renderMarkup(filePath: List[String], fileContent: String, branch: String, repository: service.RepositoryService.RepositoryInfo, diff --git a/src/main/twirl/helper/preview.scala.html b/src/main/twirl/helper/preview.scala.html index b6f37c7..3c0e13e 100644 --- a/src/main/twirl/helper/preview.scala.html +++ b/src/main/twirl/helper/preview.scala.html @@ -1,4 +1,4 @@ -@(repository: service.RepositoryService.RepositoryInfo, content: String, enableWikiLink: Boolean, enableRefsLink: Boolean, +@(repository: service.RepositoryService.RepositoryInfo, content: String, enableWikiLink: Boolean, enableRefsLink: Boolean, enableTaskList: Boolean, hasWritePermission: Boolean, style: String = "", placeholder: String = "Leave a comment", elastic: Boolean = false)(implicit context: app.Context) @import context._ @import view.helpers._ @@ -38,7 +38,8 @@ $.post('@url(repository)/_preview', { content : $('#content').val(), enableWikiLink : @enableWikiLink, - enableRefsLink : @enableRefsLink + enableRefsLink : @enableRefsLink, + enableTaskList : @enableTaskList }, function(data){ $('#preview-area').html(data); prettyPrint(); diff --git a/src/main/twirl/issues/commentform.scala.html b/src/main/twirl/issues/commentform.scala.html index 4ca951e..04169c7 100644 --- a/src/main/twirl/issues/commentform.scala.html +++ b/src/main/twirl/issues/commentform.scala.html @@ -10,7 +10,7 @@
    @avatar(loginAccount.get.userName, 48)
    - @helper.html.preview(repository, "", false, true, "width: 635px; height: 100px; max-height: 150px;", elastic = true) + @helper.html.preview(repository, "", false, true, true, hasWritePermission, "width: 635px; height: 100px; max-height: 150px;", elastic = true)
    @@ -28,4 +28,4 @@ $('').attr('name', 'action').val($(this).val().toLowerCase()).appendTo('form'); }); }); - \ No newline at end of file + diff --git a/src/main/twirl/issues/commentlist.scala.html b/src/main/twirl/issues/commentlist.scala.html index 979062f..ff3ff7e 100644 --- a/src/main/twirl/issues/commentlist.scala.html +++ b/src/main/twirl/issues/commentlist.scala.html @@ -16,7 +16,7 @@
    - @markdown(issue.content getOrElse "No description provided.", repository, false, true) + @markdown(issue.content getOrElse "No description provided.", repository, false, true, true, hasWritePermission)
    @@ -46,7 +46,7 @@ @if(comment.action == "commit" && comment.content.split(" ").last.matches("[a-f0-9]{40}")){ @defining(comment.content.substring(comment.content.length - 40)){ id =>
    @id.substring(0, 7)
    - @markdown(comment.content.substring(0, comment.content.length - 41), repository, false, true) + @markdown(comment.content.substring(0, comment.content.length - 41), repository, false, true, true, hasWritePermission) } } else { @if(comment.action == "refer"){ @@ -54,7 +54,7 @@ Issue #@issueId: @rest.mkString(":") } } else { - @markdown(comment.content, repository, false, true) + @markdown(comment.content, repository, false, true, true, hasWritePermission) } } @@ -134,5 +134,67 @@ } return false; }); + + var extractMarkdown = function(data){ + $('body').append('
    '); + $('#tmp').html(data); + var markdown = $('#tmp textarea').val(); + $('#tmp').remove(); + return markdown; + }; + + var replaceTaskList = function(issueContentHtml, checkboxes) { + var ss = [], + markdown = extractMarkdown(issueContentHtml), + xs = markdown.split(/- \[[x| ]\]/g); + for (var i=0; i \ No newline at end of file + diff --git a/src/main/twirl/issues/create.scala.html b/src/main/twirl/issues/create.scala.html index cf91159..7080667 100644 --- a/src/main/twirl/issues/create.scala.html +++ b/src/main/twirl/issues/create.scala.html @@ -57,7 +57,7 @@
    - @helper.html.preview(repository, "", false, true, "width: 565px; height: 200px; max-height: 250px;", elastic = true) + @helper.html.preview(repository, "", false, true, true, hasWritePermission, "width: 565px; height: 200px; max-height: 250px;", elastic = true)
    diff --git a/src/main/twirl/pulls/compare.scala.html b/src/main/twirl/pulls/compare.scala.html index b99e5b2..7d900ab 100644 --- a/src/main/twirl/pulls/compare.scala.html +++ b/src/main/twirl/pulls/compare.scala.html @@ -58,7 +58,7 @@
    - @helper.html.preview(repository, "", false, true, "width: 580px; height: 200px;") + @helper.html.preview(repository, "", false, true, true, hasWritePermission, "width: 580px; height: 200px;") diff --git a/src/main/twirl/wiki/edit.scala.html b/src/main/twirl/wiki/edit.scala.html index b7ca333..36a61dc 100644 --- a/src/main/twirl/wiki/edit.scala.html +++ b/src/main/twirl/wiki/edit.scala.html @@ -22,7 +22,7 @@
    - @helper.html.preview(repository, page.map(_.content).getOrElse(""), true, false, "width: 850px; height: 400px;", "") + @helper.html.preview(repository, page.map(_.content).getOrElse(""), true, false, false, false, "width: 850px; height: 400px;", "") @@ -36,4 +36,4 @@ return confirm('Are you sure you want to delete this page?'); }); }); - \ No newline at end of file + diff --git a/src/main/webapp/assets/common/css/gitbucket.css b/src/main/webapp/assets/common/css/gitbucket.css index d84bc08..c8c972d 100644 --- a/src/main/webapp/assets/common/css/gitbucket.css +++ b/src/main/webapp/assets/common/css/gitbucket.css @@ -877,6 +877,20 @@ background-color: white; } +ul.task-list { + padding-left: 2em; + margin-left: 0; +} + +li.task-list-item { + list-style-type: none; +} + +li.task-list-item input.task-list-item-checkbox { + margin: 0 4px 0.25em -20px; + vertical-align: middle; +} + /****************************************************************************/ /* Pull Request */ /****************************************************************************/ diff --git a/src/test/scala/view/GitBucketHtmlSerializerSpec.scala b/src/test/scala/view/GitBucketHtmlSerializerSpec.scala index 946b626..687db70 100644 --- a/src/test/scala/view/GitBucketHtmlSerializerSpec.scala +++ b/src/test/scala/view/GitBucketHtmlSerializerSpec.scala @@ -25,4 +25,69 @@ after mustEqual "foo%21bar%40baz%3e9000" } } + + "escapeTaskList" should { + "convert '- [ ] ' to '* task: :'" in { + val before = "- [ ] aaaa" + val after = escapeTaskList(before) + after mustEqual "* task: : aaaa" + } + + "convert ' - [ ] ' to ' * task: :'" in { + val before = " - [ ] aaaa" + val after = escapeTaskList(before) + after mustEqual " * task: : aaaa" + } + + "convert only first '- [ ] '" in { + val before = " - [ ] aaaa - [ ] bbb" + val after = escapeTaskList(before) + after mustEqual " * task: : aaaa - [ ] bbb" + } + + "convert '- [x] ' to '* task:x:'" in { + val before = " - [x] aaaa" + val after = escapeTaskList(before) + after mustEqual " * task:x: aaaa" + } + + "convert multi lines" in { + val before = """ +tasks +- [x] aaaa +- [ ] bbb +""" + val after = escapeTaskList(before) + after mustEqual """ +tasks +* task:x: aaaa +* task: : bbb +""" + } + + "no convert if inserted before '- [ ] '" in { + val before = " a - [ ] aaaa" + val after = escapeTaskList(before) + after mustEqual " a - [ ] aaaa" + } + + "no convert '- [] '" in { + val before = " - [] aaaa" + val after = escapeTaskList(before) + after mustEqual " - [] aaaa" + } + + "no convert '- [ ]a'" in { + val before = " - [ ]a aaaa" + val after = escapeTaskList(before) + after mustEqual " - [ ]a aaaa" + } + + "no convert '-[ ] '" in { + val before = " -[ ] aaaa" + val after = escapeTaskList(before) + after mustEqual " -[ ] aaaa" + } + } } +