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

http://github.com/scalate/scalate · Scala · 252 lines · 153 code · 36 blank · 63 comment · 17 complexity · 09aabe833d4fb67c90ee1e6fe6d00f27 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 collection.mutable.HashMap
  20. import org.eclipse.mylyn.wikitext.core.parser.DocumentBuilder.BlockType
  21. import org.eclipse.mylyn.internal.wikitext.confluence.core.block.ParameterizedBlock
  22. import io.Source
  23. import java.io.File
  24. import java.util.regex.{ Pattern, Matcher }
  25. import org.eclipse.mylyn.wikitext.core.parser.{ DocumentBuilder, Attributes }
  26. import org.fusesource.scalate.util.Log
  27. /**
  28. * Helper class to access file containing snippets of code:
  29. * - on the local file system
  30. * - using a full URL
  31. * - using a URL that starts with a predefined prefix
  32. */
  33. object Snippets {
  34. val log = Log(getClass); import log._
  35. var errorHandler: (SnippetBlock, Throwable) => Unit = logError
  36. var failOnError = false
  37. /**
  38. * By default lets use Pygmentize if its installed to style snippets
  39. * unless its explicitly disabled via user
  40. */
  41. var usePygmentize: Boolean = Pygmentize.isInstalled
  42. val prefixes = HashMap[String, String]()
  43. /**
  44. * Add a prefix definition to make it easier to access snippets.
  45. * E.g. if you add 'activemq' -> 'http://svn.apache.org/repos/asf/activemq/trunk'
  46. * you can use 'activemq/pom.xml' as a shorthand notation for 'http://svn.apache.org/repos/asf/activemq/trunk/pom.xml"
  47. *
  48. *
  49. * @param prefix the prefix
  50. * @param location the full URL that should be used for the prefix
  51. */
  52. def addPrefix(prefix: String, location: String): Unit = prefixes.put(prefix, location)
  53. private[wikitext] def handlePrefix(location: String): String = {
  54. val prefix = location.substring(0, location.indexOf("/"))
  55. if (prefixes.contains(prefix)) {
  56. location.replaceFirst(prefix, prefixes(prefix))
  57. } else {
  58. location
  59. }
  60. }
  61. /**
  62. * Get the snippet file contents
  63. */
  64. def getSource(location: String): Source = {
  65. val url = handlePrefix(location)
  66. def isUrl = url.indexOf(':') > 0
  67. var file = new File(location)
  68. if (!file.exists) {
  69. file = new File(url)
  70. }
  71. debug("for location: " + location + " using prefix: " + url)
  72. if (file.exists || !isUrl) {
  73. Source.fromFile(file, "UTF-8")
  74. } else {
  75. Source.fromURL(url, "UTF-8")
  76. }
  77. }
  78. protected def logError(snippet: SnippetBlock, e: Throwable): Unit = {
  79. error(e, "Failed to generate snippet: " + snippet.url + ". " + e)
  80. if (failOnError) {
  81. throw e
  82. }
  83. }
  84. }
  85. import Snippets.log._
  86. /**
  87. * Represents a {snippet} block in the wiki markup
  88. */
  89. class SnippetBlock extends ParameterizedBlock {
  90. var pattern: Pattern = Pattern.compile("\\s*\\{snippet(?::([^\\}]*))?\\}(.*)")
  91. var matcher: Matcher = null
  92. var lang: Option[String] = None
  93. var url: String = _
  94. var id: Option[String] = None
  95. var pygmentize: Boolean = Snippets.usePygmentize
  96. lazy val handler: SnippetHandler = {
  97. if (pygmentize) {
  98. val block = new PygmentsBlock
  99. block.setState(state)
  100. block.setParser(parser)
  101. block.setOption("lang", language)
  102. PygmentizeSnippetHandler(block)
  103. } else {
  104. DefaultSnippetHandler(builder, language)
  105. }
  106. }
  107. override def canStart(line: String, lineOffset: Int) = {
  108. matcher = pattern.matcher(line)
  109. if (lineOffset > 0) {
  110. matcher.region(lineOffset, line.length)
  111. }
  112. matcher.matches()
  113. }
  114. override def processLineContent(line: String, offset: Int) = {
  115. setOptions(matcher.group(1))
  116. val end = matcher.start(2)
  117. handler.begin
  118. try {
  119. for (snippetLine <- getSnippet) {
  120. handler.addLine(snippetLine)
  121. }
  122. } catch {
  123. case e: Exception => Snippets.errorHandler(this, e)
  124. }
  125. handler.done
  126. if (end < line.length) {
  127. state.setLineSegmentEndOffset(end)
  128. }
  129. setClosed(true)
  130. if (end == line.length) { -1 } else { end }
  131. }
  132. /**
  133. * Extract the snippet from the Source file
  134. */
  135. def getSnippet: Iterator[String] = {
  136. val lines = Snippets.getSource(url).getLines
  137. id match {
  138. case None => lines
  139. case Some(snippet) => lines.dropWhile(!_.contains("START SNIPPET: " + snippet))
  140. .takeWhile(!_.contains("END SNIPPET: " + snippet))
  141. .drop(1)
  142. }
  143. }
  144. override def setOption(key: String, value: String) = {
  145. key match {
  146. case "id" => id = Some(value)
  147. case "url" => url = value
  148. case "lang" => lang = Some(value)
  149. case "pygmentize" => pygmentize = value.toBoolean
  150. case n => warn("Ignored snippet attribute %s on %s", n, this)
  151. }
  152. }
  153. /**
  154. * Get the configured language or determine the language from the file extension
  155. */
  156. def language = {
  157. lang match {
  158. case None => extension(url)
  159. case Some(lang) => lang
  160. }
  161. }
  162. /**
  163. * Extract the file extension from the URL
  164. */
  165. def extension(url: String) = {
  166. if (url.contains(".")) {
  167. url.split('.').last
  168. } else {
  169. ""
  170. }
  171. }
  172. override def toString = "{snippet:url=" + url + "}"
  173. }
  174. /**
  175. * Trait to define a {snippet} handler
  176. */
  177. trait SnippetHandler {
  178. def begin(): Unit
  179. def addLine(line: String): Unit
  180. def done(): Unit
  181. }
  182. /**
  183. * Default handler for the {snippet} code (renders a <div class="snippet"><pre class="<language>">
  184. */
  185. case class DefaultSnippetHandler(
  186. val builder: DocumentBuilder,
  187. val language: String) extends SnippetHandler {
  188. def begin() = {
  189. builder.beginBlock(BlockType.DIV, cssClass("snippet"))
  190. builder.beginBlock(BlockType.PREFORMATTED, cssClass(language))
  191. builder.characters("\n")
  192. }
  193. def addLine(line: String) = {
  194. builder.characters(line + "\n")
  195. }
  196. def done() = {
  197. builder.endBlock(); // </pre>
  198. builder.endBlock(); // </div>
  199. }
  200. /**
  201. * Create attributes instance containing the CSS class
  202. */
  203. def cssClass(cssClass: String) = {
  204. val attributes = new Attributes
  205. attributes.setCssClass(cssClass)
  206. attributes
  207. }
  208. }
  209. /**
  210. * Uses pygmentize to handles syntax coloring for the {snippet}'s code
  211. */
  212. case class PygmentizeSnippetHandler(val block: PygmentsBlock) extends SnippetHandler {
  213. def begin() = block.beginBlock
  214. def addLine(line: String) = block.handleBlockContent(line)
  215. def done() = block.endBlock
  216. }