/scalate-tool/src/main/scala/org/fusesource/scalate/tool/commands/ToScaml.scala

http://github.com/scalate/scalate · Scala · 335 lines · 265 code · 42 blank · 28 comment · 30 complexity · 2e1dbecd4915308a8a4b2f1806ac42ab 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.tool.commands
  19. import org.apache.felix.gogo.commands.{Action, Option => option, Argument => argument, Command => command}
  20. import scala.xml._
  21. import java.io._
  22. import java.net.URL
  23. import org.fusesource.scalate.util.IOUtil
  24. import org.fusesource.scalate.InvalidSyntaxException
  25. import util.parsing.input.CharSequenceReader
  26. import org.fusesource.scalate.support.{Text=>SSPText, ScalaParseSupport}
  27. import org.fusesource.scalate.ssp._
  28. import org.w3c.tidy.{TidyMessage, TidyMessageListener, Tidy}
  29. import org.apache.felix.service.command.CommandSession
  30. /* an even simpler ssp parser */
  31. class SspParser extends ScalaParseSupport {
  32. var skipWhitespaceOn = false
  33. override def skipWhitespace = skipWhitespaceOn
  34. def skip_whitespace[T](p: => Parser[T]): Parser[T] = Parser[T] {
  35. in =>
  36. skipWhitespaceOn = true
  37. val result = p(in)
  38. skipWhitespaceOn = false
  39. result
  40. }
  41. val anySpace = text("""[ \t]*""".r)
  42. val identifier = text("""[a-zA-Z0-9\$_]+""".r)
  43. val typeName = text(scalaType)
  44. val someText = text(""".+""".r)
  45. val attribute = skip_whitespace(opt(text("import")) ~ text("var" | "val") ~ identifier ~ (":" ~> typeName)) ~ ("""\s*""".r ~> opt("""=\s*""".r ~> upto("""\s*%>""".r))) ^^ {
  46. case (p_import ~ p_kind ~ p_name ~ p_type) ~ p_default => ScriptletFragment(p_kind+" "+p_name+":"+p_type+" //attribute")
  47. }
  48. val literalPart: Parser[SSPText] =
  49. upto("<%" | """\<%""" | """\\<%""" | "${" | """\${""" | """\\${""" | """\#""" | """\\#""" | directives) ~
  50. opt(
  51. """\<%""" ~ opt(literalPart) ^^ {case x ~ y => "<%" + y.getOrElse("")} |
  52. """\${""" ~ opt(literalPart) ^^ {case x ~ y => "${" + y.getOrElse("")} |
  53. """\#""" ~ opt(literalPart) ^^ {case x ~ y => "#" + y.getOrElse("")} |
  54. """\\""" ^^ {s => """\"""}
  55. ) ^^ {
  56. case x ~ Some(y) => x + y
  57. case x ~ None => x
  58. }
  59. val tagEnding = "+%>" | """%>[ \t]*\r?\n""".r | "%>"
  60. val commentFragment = wrapped("<%--", "--%>") ^^ {CommentFragment(_)}
  61. val altCommentFragment = wrapped("<%#", "%>") ^^ {CommentFragment(_)}
  62. val dollarExpressionFragment = wrapped("${", "}") ^^ {ExpressionFragment(_)}
  63. val expressionFragment = wrapped("<%=", "%>") ^^ {ExpressionFragment(_)}
  64. val attributeFragement = prefixed("<%@", attribute <~ anySpace ~ tagEnding)
  65. val scriptletFragment = wrapped("<%", tagEnding) ^^ {ScriptletFragment(_)}
  66. val textFragment = literalPart ^^ {TextFragment(_)}
  67. def directives = ("#" ~> identifier ~ anySpace ~ opt("(" ~> scalaExpression <~ ")")) ^^ {
  68. case a ~ b ~ c => ScriptletFragment(a+c.map("("+_+")").getOrElse(""))
  69. } | "#(" ~> identifier <~ ")" ^^ {ScriptletFragment(_)}
  70. def scalaExpression: Parser[SSPText] = {
  71. text(
  72. (rep(nonParenText) ~ opt("(" ~> scalaExpression <~ ")") ~ rep(nonParenText)) ^^ {
  73. case a ~ b ~ c =>
  74. val mid = b match {
  75. case Some(tb) => "(" + tb + ")"
  76. case tb => ""
  77. }
  78. a.mkString("") + mid + c.mkString("")
  79. })
  80. }
  81. val nonParenText = characterLiteral | stringLiteral | """[^\(\)\'\"]+""".r
  82. val pageFragment: Parser[PageFragment] = directives | commentFragment | altCommentFragment | dollarExpressionFragment |
  83. attributeFragement | expressionFragment | scriptletFragment |
  84. textFragment
  85. val pageFragments = rep(pageFragment)
  86. private def phraseOrFail[T](p: Parser[T], in: String): T = {
  87. var x = phrase(p)(new CharSequenceReader(in))
  88. x match {
  89. case Success(result, _) => result
  90. case NoSuccess(message, next) => throw new InvalidSyntaxException(message, next.pos);
  91. }
  92. }
  93. def getPageFragments(in: String): List[PageFragment] = {
  94. phraseOrFail(pageFragments, in)
  95. }
  96. }
  97. /**
  98. * <p>
  99. * </p>
  100. *
  101. * @author <a href="http://hiramchirino.com">Hiram Chirino</a>
  102. */
  103. @command(scope = "scalate", name = "toscaml", description = "Converts an XML or HTML file to Scaml")
  104. class ToScaml extends Action {
  105. @option(name = "--tidy", description = "Should html be tidied first?")
  106. var tidy = true
  107. @argument(index = 0, name = "from", description = "The input file or http URL. If ommited, input is read from the console")
  108. var from: String = _
  109. @argument(index = 1, name = "to", description = "The output file. If ommited, output is written to the console")
  110. var to: File = _
  111. var out:IndentPrintStream = _
  112. def execute(session: CommandSession): AnyRef = {
  113. def doit:Unit = {
  114. var in = if( from==null ) {
  115. session.getKeyboard
  116. } else {
  117. if( from.startsWith("http://") || from.startsWith("https://") ) {
  118. new URL(from).openStream
  119. } else {
  120. new FileInputStream(from)
  121. }
  122. }
  123. var data = IOUtil.loadBytes(in)
  124. //println("original: "+new String(data, "UTF-8"))
  125. // Parse out the code bits and wrap them in script tags so that
  126. // we can tidy the document.
  127. val fragments = (new SspParser).getPageFragments(new String(data, "UTF-8"))
  128. data = ("<div>" + (fragments.map(_ match {
  129. case ExpressionFragment(code) => "#{" + code.value + "}"
  130. case ScriptletFragment(code) => """<scriptlet><![CDATA[""" + code.value + """]]></scriptlet>"""
  131. case CommentFragment(comment) => """<!--""" + comment.value + """-->"""
  132. case TextFragment(text) => text.value
  133. case unexpected: PageFragment =>
  134. System.err.println("Unexpected page fragment " + unexpected)
  135. "" // skip it
  136. }).mkString("")) + "</div>").getBytes("UTF-8")
  137. // println("escaped: "+new String(data, "UTF-8"))
  138. // try to tidy the html first before we try to parse it as XML
  139. if (tidy) {
  140. val tidy = new Tidy
  141. tidy.setXHTML(true)
  142. tidy.setXmlTags(true)
  143. tidy.setIndentCdata(false)
  144. tidy.setEscapeCdata(false)
  145. tidy.setQuiet(true)
  146. val out = new ByteArrayOutputStream()
  147. tidy.parse(new ByteArrayInputStream(data), out);
  148. data = out.toByteArray
  149. // println("tidy: "+new String(data, "UTF-8"))
  150. }
  151. // Try to strip out the doc type... stuff..
  152. {
  153. val text = new String(data, "UTF-8").trim
  154. if( text.startsWith("<!DOCTYPE") ) {
  155. data = text.substring(text.indexOf('>')+1).getBytes("UTF-8")
  156. // println("doctype: "+new String(data, "UTF-8"))
  157. }
  158. }
  159. val doc = try {
  160. XML.load(new ByteArrayInputStream(data))
  161. } catch {
  162. case e:SAXParseException =>
  163. // save the tidy version...
  164. System.err.println("Could not parse the html markup: "+e.getMessage+" at "+e.getLineNumber+":"+e.getColumnNumber)
  165. out.write(data)
  166. return
  167. case e:Throwable =>
  168. // save the tidy version...
  169. System.err.println("Could not parse the html markup: "+e.getMessage)
  170. out.write(data)
  171. return
  172. }
  173. doc.child.foreach(process(_))
  174. }
  175. if( to!=null ) {
  176. out = new IndentPrintStream(new FileOutputStream(to));
  177. doit
  178. out.close()
  179. } else {
  180. out = new IndentPrintStream(session.getConsole);
  181. doit
  182. out.flush()
  183. }
  184. null
  185. }
  186. def to_text(line: String): String = {
  187. line
  188. }
  189. def to_element(tag: String): String = {
  190. var rc = tag
  191. if( rc.startsWith("div.") || tag.startsWith("div#") ) {
  192. rc = rc.stripPrefix("div")
  193. }
  194. "%"+rc
  195. }
  196. def process(value:AnyRef):Unit = {
  197. val t = out
  198. import t._
  199. def tag(name:String) = {
  200. if( name.matches("""^[\w:_\-]+$""") ) {
  201. name
  202. } else {
  203. "'"+name+"'"
  204. }
  205. }
  206. value match {
  207. case x:Elem =>
  208. var id=""
  209. var clazz=""
  210. var atts=""
  211. def add(key:String, value:String) = {
  212. if( atts!="" ) {
  213. atts += " "
  214. }
  215. atts += key+"=\""+value+"\""
  216. }
  217. x.attributes.foreach{ a=>
  218. val key = a.key
  219. val value = a.value.toString
  220. if( key=="id" ) {
  221. if( value.matches("""^[\w_\-]+$""") )
  222. id = "#"+value
  223. else
  224. add(key,value)
  225. } else if( key=="class" ) {
  226. if( value.matches("""^[\w\s_\-]+$""") ) {
  227. value.split("""\s""").foreach{ c=>
  228. clazz += "."+c
  229. }
  230. } else {
  231. add(key,value)
  232. }
  233. } else {
  234. add(key,value)
  235. }
  236. }
  237. if(x.label=="scriptlet") {
  238. for( line <- x.child.text.trim().split("""\r?\n""").filter( _.length()!=0) ) {
  239. pi.pl("- "+line)
  240. }
  241. } else {
  242. pi.p(to_element(tag(x.label)+id+clazz))
  243. if( atts!="" ) {
  244. p("("+atts+")")
  245. }
  246. x.child match {
  247. case Seq(x:Text) =>
  248. val value = x.text.trim
  249. if (value.contains("\n")) {
  250. pl()
  251. indent {
  252. process(x)
  253. }
  254. } else {
  255. pl(" "+value)
  256. }
  257. case x =>
  258. pl()
  259. indent {
  260. x.foreach{ process _ }
  261. }
  262. }
  263. }
  264. case x:Text =>
  265. val value = x.text.trim
  266. value.split("\r?\n").map(_.trim).foreach{ line =>
  267. if(line != "" ) {
  268. pi.pl(to_text(line))
  269. }
  270. }
  271. case x:AnyRef =>
  272. throw new Exception("Unhandled type: "+x.getClass);
  273. }
  274. }
  275. class IndentPrintStream(out:OutputStream) extends PrintStream(out) {
  276. var level=0
  277. def indent[T](op: => T): T = {level += 1; val rc = op; level -= 1; rc}
  278. def pi = { for (i <- 0 until level) { print(" ") }; this }
  279. def p(line: String) = { print(line); this }
  280. def pl(line: String="") = { println(line); this }
  281. }
  282. }