PageRenderTime 41ms CodeModel.GetById 17ms app.highlight 19ms RepoModel.GetById 2ms app.codeStats 0ms

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