diff --git a/src/main/scala/gitbucket/core/view/Markdown.scala b/src/main/scala/gitbucket/core/view/Markdown.scala index 9be7ae9..2e126eb 100644 --- a/src/main/scala/gitbucket/core/view/Markdown.scala +++ b/src/main/scala/gitbucket/core/view/Markdown.scala @@ -1,6 +1,7 @@ package gitbucket.core.view import java.text.Normalizer +import java.util.regex.Pattern import java.util.{Optional, Locale} import gitbucket.core.controller.Context @@ -19,7 +20,7 @@ * @param enableRefsLink if true then issue reference (e.g. #123) is rendered as link * @param enableAnchor if true then anchor for headline is generated * @param enableTaskList if true then task list syntax is available - * @param hasWritePermission + * @param hasWritePermission true if user has writable to ths given repository * @param pages the list of existing Wiki pages */ def toHtml(markdown: String, @@ -37,8 +38,7 @@ // escape task list val source = if(enableTaskList){ - s.replaceAll("""(?m)^( *)- \[([x| ])\] """, "$1* task:$2: ") -// escapeTaskList(s) + escapeTaskList(s) } else s val options = new Options() @@ -46,133 +46,137 @@ Marked.marked(source, options, renderer) } -// private def escapeTaskList(text: String): String = { -// Pattern.compile("""^( *)- \[([x| ])\] """, Pattern.MULTILINE).matcher(text).replaceAll("$1* task:$2: ") -// } + /** + * Extends markedj Renderer for GitBucket + */ + class GitBucketMarkedRenderer(options: Options, repository: RepositoryService.RepositoryInfo, + enableWikiLink: Boolean, enableRefsLink: Boolean, enableAnchor: Boolean, enableTaskList: Boolean, hasWritePermission: Boolean, + pages: List[String]) + (implicit val context: Context) extends Renderer(options) with LinkConverter with RequestCache { -} + override def heading(text: String, level: Int, raw: String): String = { + val id = generateAnchorName(text) + val out = new StringBuilder() -class GitBucketMarkedRenderer(options: Options, repository: RepositoryService.RepositoryInfo, - enableWikiLink: Boolean, enableRefsLink: Boolean, enableAnchor: Boolean, enableTaskList: Boolean, hasWritePermission: Boolean, - pages: List[String]) - (implicit val context: Context) extends Renderer(options) with LinkConverter with RequestCache { + out.append("") - override def heading(text: String, level: Int, raw: String): String = { - val id = generateAnchorName(text) - val out = new StringBuilder() - - out.append("") - - if(enableAnchor){ - out.append("") - out.append("") - } - - out.append(text) - out.append("\n") - out.toString() - } - - override def code(code: String, lang: Optional[String], escaped: Boolean): String = { - "
" +
-      (if(escaped) code else escape(code, true)) + "
" - } - - override def list(body: String, ordered: Boolean): String = { - var listType: String = null - if (ordered) { - listType = "ol" - } - else { - listType = "ul" - } - if(body.contains("""class="task-list-item-checkbox"""")){ - return "<" + listType + " class=\"task-list\">\n" + body + "\n" - } else { - return "<" + listType + ">\n" + body + "\n" - } - } - - override def listitem(text: String): String = { - if(text.contains("""class="task-list-item-checkbox" """)){ - return "
  • " + text + "
  • \n" - } else { - return "
  • " + text + "
  • \n" - } - } - - override def text(text: String): String = { - // convert commit id and username to link. - val t1 = if(enableRefsLink) convertRefsLinks(text, repository, "issue:") else text - - // convert task list to checkbox. - val t2 = if(enableTaskList) convertCheckBox(t1, hasWritePermission) else t1 - - t2 - } - - override def link(href: String, title: Optional[String], text: String): String = { - super.link(fixUrl(href, true), title, text) - } - - override def image(href: String, title: Optional[String], text: String): String = { - super.image(fixUrl(href, true), title, text) - } - - override def nolink(text: String): String = { - if(enableWikiLink && text.startsWith("[[") && text.endsWith("]]")){ - val link = text.replaceAll("(^\\[\\[|\\]\\]$)", "") - - val (label, page) = if(link.contains('|')){ - val i = link.indexOf('|') - (link.substring(0, i), link.substring(i + 1)) - } else { - (link, link) + if(enableAnchor){ + out.append("") + out.append("") } - val url = repository.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/wiki/" + StringUtil.urlEncode(page) - if(pages.contains(page)){ - "" + escape(label) + "" - } else { - "" + escape(label) + "" - } - } else { - escape(text) + out.append(text) + out.append("
    \n") + out.toString() } + + override def code(code: String, lang: Optional[String], escaped: Boolean): String = { + "
    " +
    +        (if(escaped) code else escape(code, true)) + "
    " + } + + override def list(body: String, ordered: Boolean): String = { + var listType: String = null + if (ordered) { + listType = "ol" + } + else { + listType = "ul" + } + if(body.contains("""class="task-list-item-checkbox"""")){ + return "<" + listType + " class=\"task-list\">\n" + body + "\n" + } else { + return "<" + listType + ">\n" + body + "\n" + } + } + + override def listitem(text: String): String = { + if(text.contains("""class="task-list-item-checkbox" """)){ + return "
  • " + text + "
  • \n" + } else { + return "
  • " + text + "
  • \n" + } + } + + override def text(text: String): String = { + // convert commit id and username to link. + val t1 = if(enableRefsLink) convertRefsLinks(text, repository, "issue:") else text + + // convert task list to checkbox. + val t2 = if(enableTaskList) convertCheckBox(t1, hasWritePermission) else t1 + + t2 + } + + override def link(href: String, title: Optional[String], text: String): String = { + super.link(fixUrl(href, true), title, text) + } + + override def image(href: String, title: Optional[String], text: String): String = { + super.image(fixUrl(href, true), title, text) + } + + override def nolink(text: String): String = { + if(enableWikiLink && text.startsWith("[[") && text.endsWith("]]")){ + val link = text.replaceAll("(^\\[\\[|\\]\\]$)", "") + + val (label, page) = if(link.contains('|')){ + val i = link.indexOf('|') + (link.substring(0, i), link.substring(i + 1)) + } else { + (link, link) + } + + val url = repository.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/wiki/" + StringUtil.urlEncode(page) + if(pages.contains(page)){ + "" + escape(label) + "" + } else { + "" + escape(label) + "" + } + } else { + escape(text) + } + } + + private def fixUrl(url: String, isImage: Boolean = false): String = { + if(url.startsWith("http://") || url.startsWith("https://") || url.startsWith("/")){ + url + } else if(url.startsWith("#")){ + ("#" + generateAnchorName(url.substring(1))) + } else if(!enableWikiLink){ + if(context.currentPath.contains("/blob/")){ + url + (if(isImage) "?raw=true" else "") + } else if(context.currentPath.contains("/tree/")){ + val paths = context.currentPath.split("/") + val branch = if(paths.length > 3) paths.drop(4).mkString("/") else repository.repository.defaultBranch + repository.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/blob/" + branch + "/" + url + (if(isImage) "?raw=true" else "") + } else { + val paths = context.currentPath.split("/") + val branch = if(paths.length > 3) paths.last else repository.repository.defaultBranch + repository.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/blob/" + branch + "/" + url + (if(isImage) "?raw=true" else "") + } + } else { + repository.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/wiki/_blob/" + url + } + } + } - private def fixUrl(url: String, isImage: Boolean = false): String = { - if(url.startsWith("http://") || url.startsWith("https://") || url.startsWith("/")){ - url - } else if(url.startsWith("#")){ - ("#" + generateAnchorName(url.substring(1))) - } else if(!enableWikiLink){ - if(context.currentPath.contains("/blob/")){ - url + (if(isImage) "?raw=true" else "") - } else if(context.currentPath.contains("/tree/")){ - val paths = context.currentPath.split("/") - val branch = if(paths.length > 3) paths.drop(4).mkString("/") else repository.repository.defaultBranch - repository.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/blob/" + branch + "/" + url + (if(isImage) "?raw=true" else "") - } else { - val paths = context.currentPath.split("/") - val branch = if(paths.length > 3) paths.last else repository.repository.defaultBranch - repository.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/blob/" + branch + "/" + url + (if(isImage) "?raw=true" else "") - } - } else { - repository.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/wiki/_blob/" + url - } + def escapeTaskList(text: String): String = { + Pattern.compile("""^( *)- \[([x| ])\] """, Pattern.MULTILINE).matcher(text).replaceAll("$1* task:$2: ") } - private def generateAnchorName(text: String): String = { + def generateAnchorName(text: String): String = { val normalized = Normalizer.normalize(text.replaceAll("<.*>", "").replaceAll("[\\s]", "-"), Normalizer.Form.NFD) val encoded = StringUtil.urlEncode(normalized) encoded.toLowerCase(Locale.ENGLISH) } - private def convertCheckBox(text: String, hasWritePermission: Boolean): String = { + def convertCheckBox(text: String, hasWritePermission: Boolean): String = { val disabled = if (hasWritePermission) "" else "disabled" text.replaceAll("task:x:", """") .replaceAll("task: :", """") } } + diff --git a/src/test/scala/gitbucket/core/view/GitBucketHtmlSerializerSpec.scala b/src/test/scala/gitbucket/core/view/GitBucketHtmlSerializerSpec.scala deleted file mode 100644 index 82fb3d9..0000000 --- a/src/test/scala/gitbucket/core/view/GitBucketHtmlSerializerSpec.scala +++ /dev/null @@ -1,93 +0,0 @@ -package gitbucket.core.view - -import org.specs2.mutable._ - -class GitBucketHtmlSerializerSpec extends Specification { - - import GitBucketHtmlSerializer._ - - "generateAnchorName" should { - "convert whitespace characters to hyphens" in { - val before = "foo bar baz" - val after = generateAnchorName(before) - after mustEqual "foo-bar-baz" - } - - "normalize characters with diacritics" in { - val before = "Dónde estará mi vida" - val after = generateAnchorName(before) - after mustEqual "do%cc%81nde-estara%cc%81-mi-vida" - } - - "omit special characters" in { - val before = "foo!bar@baz>9000" - val after = generateAnchorName(before) - 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" - } - } -} - diff --git a/src/test/scala/gitbucket/core/view/MarkdownSpec.scala b/src/test/scala/gitbucket/core/view/MarkdownSpec.scala new file mode 100644 index 0000000..a2ec2e9 --- /dev/null +++ b/src/test/scala/gitbucket/core/view/MarkdownSpec.scala @@ -0,0 +1,93 @@ +package gitbucket.core.view + +import org.specs2.mutable._ + +class MarkdownSpec extends Specification { + + import Markdown._ + + "generateAnchorName" should { + "convert whitespace characters to hyphens" in { + val before = "foo bar baz" + val after = generateAnchorName(before) + after mustEqual "foo-bar-baz" + } + + "normalize characters with diacritics" in { + val before = "Dónde estará mi vida" + val after = generateAnchorName(before) + after mustEqual "do%cc%81nde-estara%cc%81-mi-vida" + } + + "omit special characters" in { + val before = "foo!bar@baz>9000" + val after = generateAnchorName(before) + 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" + } + } +} +