/scalate-wikitext/src/main/scala/org/fusesource/scalate/wikitext/PygmentsBlock.scala

http://github.com/scalate/scalate · Scala · 296 lines · 207 code · 55 blank · 34 comment · 14 complexity · 1dbc590a708f3f26563001317b65a191 MD5 · raw file

  1. /**
  2. * Copyright (C) 2009-2011 the original author or authors.
  3. * See the notice.md file distributed with this work for additional
  4. * information regarding copyright ownership.
  5. *
  6. * Licensed under the Apache License, Version 2.0 (the "License");
  7. * you may not use this file except in compliance with the License.
  8. * You may obtain a copy of the License at
  9. *
  10. * http://www.apache.org/licenses/LICENSE-2.0
  11. *
  12. * Unless required by applicable law or agreed to in writing, software
  13. * distributed under the License is distributed on an "AS IS" BASIS,
  14. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  15. * See the License for the specific language governing permissions and
  16. * limitations under the License.
  17. */
  18. package org.fusesource.scalate.wikitext
  19. import org.eclipse.mylyn.wikitext.core.parser.Attributes
  20. import org.eclipse.mylyn.wikitext.core.parser.DocumentBuilder.BlockType
  21. import org.eclipse.mylyn.internal.wikitext.confluence.core.block.AbstractConfluenceDelimitedBlock
  22. import java.lang.String
  23. import collection.mutable.ListBuffer
  24. import org.fusesource.scalate.util.Threads._
  25. import util.parsing.input.CharSequenceReader
  26. import util.parsing.combinator.RegexParsers
  27. import org.fusesource.scalate.support.RenderHelper
  28. import org.fusesource.scalate._
  29. import org.fusesource.scalate.filter.Filter
  30. import java.io.{ File, ByteArrayInputStream, ByteArrayOutputStream }
  31. import util.{ Files, Log, IOUtil }
  32. object Pygmentize extends Log with Filter with TemplateEngineAddOn {
  33. /**
  34. * Add the markdown filter to the template engine.
  35. */
  36. def apply(te: TemplateEngine) = {
  37. te.filters += "pygmentize" -> Pygmentize
  38. // add the imports
  39. te.importStatements :+= "import org.fusesource.scalate.wikitext.PygmentizeHelpers._"
  40. }
  41. def filter(context: RenderContext, content: String): String = {
  42. pygmentize(content)
  43. }
  44. // lets calculate once on startup
  45. private[this] lazy val _installedVersion: Option[String] = {
  46. try {
  47. val process = Runtime.getRuntime.exec(Array("pygmentize", "-V"))
  48. thread("pygmentize err handler") {
  49. IOUtil.copy(process.getErrorStream, System.err)
  50. }
  51. val out = new ByteArrayOutputStream()
  52. thread("pygmentize out handler") {
  53. IOUtil.copy(process.getInputStream, out)
  54. }
  55. process.waitFor
  56. if (process.exitValue != 0) {
  57. None
  58. } else {
  59. val output = new String(out.toByteArray).trim
  60. debug("Pygmentize installed: " + output)
  61. val version = output.split("[ ,]")(2)
  62. Some(version)
  63. }
  64. } catch {
  65. case e: Exception =>
  66. debug(e, "Failed to start pygmentize: " + e)
  67. None
  68. }
  69. }
  70. def isInstalled: Boolean = _installedVersion match {
  71. case Some(_) => true
  72. case None => false
  73. }
  74. def version: String = _installedVersion getOrElse ""
  75. def majorVersion: Int = version(0).asDigit
  76. def unindent(data: String): String = unindent(data.split("""\r?\n""").toList)
  77. def unindent(data: collection.Seq[String]): String = {
  78. var content = data
  79. // To support indenting the macro.. we figure out the indent level of the
  80. // code block by looking at the indent of the last line
  81. val indent_re = """^([ \t]+)$""".r
  82. content.lastOption match {
  83. case Some(indent_re(indent)) =>
  84. // strip off those indents.
  85. content = content.map(_.replaceFirst("""^[ \t]{""" + indent.size + """}""", ""))
  86. case _ =>
  87. }
  88. content.mkString("\n")
  89. }
  90. object OptionParser extends RegexParsers {
  91. override def skipWhitespace = false
  92. val lang = """[\w0-9_-]+""".r
  93. val key = """[\w0-9_-]+""".r
  94. val value = """[\w0-9_-]+""".r
  95. val attributes = repsep(key ~ ("=" ~> value), whiteSpace) ^^ { list =>
  96. var rc = Map[String, String]()
  97. for ((x ~ y) <- list) {
  98. rc += x -> y
  99. }
  100. rc
  101. }
  102. val option_line: Parser[(Option[String], Map[String, String])] =
  103. guard(key ~ "=") ~> attributes <~ opt(whiteSpace) ^^ { case y => (None, y) } |
  104. lang ~ opt(whiteSpace ~> attributes <~ opt(whiteSpace)) ^^ {
  105. case x ~ Some(y) => (Some(x), y)
  106. case x ~ None => (Some(x), Map())
  107. }
  108. def apply(in: String) = {
  109. (phrase(opt(whiteSpace) ~> option_line)(new CharSequenceReader(in))) match {
  110. case Success(result, _) => Some(result)
  111. // case NoSuccess(message, next) => throw new Exception(message+" at "+next.pos)
  112. case NoSuccess(message, next) => None
  113. }
  114. }
  115. }
  116. def pygmentize(data: String, options: String = ""): String = {
  117. var lang1 = "text"
  118. var lines = false
  119. var wide = false
  120. val opts = OptionParser(options)
  121. opts match {
  122. case Some((lang, atts)) =>
  123. lang1 = lang.getOrElse(lang1)
  124. for ((key, value) <- atts) {
  125. key match {
  126. case "lines" => lines = java.lang.Boolean.parseBoolean(value)
  127. case "wide" => wide = java.lang.Boolean.parseBoolean(value)
  128. }
  129. }
  130. case _ =>
  131. }
  132. // Now look for header sections...
  133. val header_re = """(?s)\n------+\s*\n\s*([^:\s]+)\s*:\s*([^\n]+)\n------+\s*\n(.*)""".r
  134. header_re.findFirstMatchIn("\n" + data) match {
  135. case Some(m1) =>
  136. lang1 = m1.group(1)
  137. val title1 = m1.group(2)
  138. var data1 = m1.group(3)
  139. header_re.findFirstMatchIn(data1) match {
  140. case Some(m2) =>
  141. data1 = data1.substring(0, m2.start)
  142. val lang2 = m2.group(1)
  143. val title2 = m2.group(2)
  144. val data2 = m2.group(3)
  145. val colored1 = pygmentize(data1, lang1, lines)
  146. val colored2 = pygmentize(data2, lang2, lines)
  147. var rc = """<div class="compare"><div class="compare-left"><h3>%s</h3><div class="syntax">%s</div></div><div class="compare-right"><h3>%s</h3><div class="syntax">%s</div></div><br class="clear"/></div>
  148. |""".stripMargin.format(title1, colored1, title2, colored2)
  149. if (wide) {
  150. rc = """<div class="wide">%s</div>""".format(rc)
  151. }
  152. rc
  153. case None =>
  154. """<div class="compare"><h3>%s</h3><div class="syntax">%s</div></div>
  155. |""".stripMargin.format(title1, pygmentize(data1, lang1, lines))
  156. }
  157. case None =>
  158. """<div class="syntax">%s</div>
  159. |""".stripMargin.format(pygmentize(data, lang1, lines))
  160. }
  161. }
  162. def pygmentize(body: String, lang: String, lines: Boolean): String = {
  163. if (!isInstalled) {
  164. "<pre name='code' class='brush: " + lang + "; gutter: " + lines + ";'><code>" + RenderHelper.sanitize(body) + "</code></pre>"
  165. } else {
  166. var options = "style=colorful"
  167. if (lines) {
  168. options += ",linenos=1"
  169. }
  170. val process = Runtime.getRuntime.exec(Array("pygmentize", "-O", options, "-f", "html", "-l", lang))
  171. thread("pygmentize err handler") {
  172. IOUtil.copy(process.getErrorStream, System.err)
  173. }
  174. thread("pygmentize in handler") {
  175. IOUtil.copy(new ByteArrayInputStream(body.getBytes), process.getOutputStream)
  176. process.getOutputStream.close
  177. }
  178. val out = new ByteArrayOutputStream()
  179. IOUtil.copy(process.getInputStream, out)
  180. process.waitFor
  181. if (process.exitValue != 0) {
  182. throw new RuntimeException("'pygmentize' execution failed: %d. Did you install it from http://pygments.org/download/ ?".format(process.exitValue))
  183. }
  184. new String(out.toByteArray).replaceAll("""\r?\n""", "&#x000A;")
  185. }
  186. }
  187. }
  188. /**
  189. * View helper methods for use inside templates
  190. */
  191. object PygmentizeHelpers {
  192. // TODO add the text version and the macro version......
  193. // TODO is there a simpler polymophic way to write functions like this
  194. // that operate on text content from a String, block, URI, File, Resource etc...
  195. def pygmentizeFile(file: File, lang: String = "", lines: Boolean = false): String = {
  196. val content = IOUtil.loadTextFile(file)
  197. val defaultLang = getOrUseExtension(lang, file.toString)
  198. Pygmentize.pygmentize(content, defaultLang)
  199. }
  200. def pygmentizeUri(uri: String, lang: String = "", lines: Boolean = false)(implicit resourceContext: RenderContext): String = {
  201. val content = resourceContext.load(uri)
  202. val defaultLang = getOrUseExtension(lang, uri)
  203. Pygmentize.pygmentize(content, defaultLang)
  204. }
  205. protected def getOrUseExtension(lang: String, uri: String): String = {
  206. if (lang.isEmpty) {
  207. Files.extension(uri)
  208. } else {
  209. lang
  210. }
  211. }
  212. }
  213. class PygmentsBlock extends AbstractConfluenceDelimitedBlock("pygmentize") {
  214. var language: String = _
  215. var lines: Boolean = false
  216. var content = ListBuffer[String]()
  217. override def beginBlock() = {
  218. val attributes = new Attributes();
  219. attributes.setCssClass("syntax");
  220. builder.beginBlock(BlockType.DIV, attributes);
  221. }
  222. override def handleBlockContent(value: String) = {
  223. // collect all the content lines..
  224. content += value
  225. }
  226. override def endBlock() = {
  227. import Pygmentize._
  228. builder.charactersUnescaped(pygmentize(unindent(content), language, lines))
  229. content.clear
  230. builder.endBlock();
  231. }
  232. override def setOption(option: String) = {
  233. language = option.toLowerCase();
  234. }
  235. override def setOption(key: String, value: String) = {
  236. key match {
  237. case "lines" => lines = value == "true"
  238. case "lang" => language = value
  239. }
  240. }
  241. }