/scalate-core/src/main/scala/org/fusesource/scalate/scaml/ScamlCodeGenerator.scala

http://github.com/scalate/scalate · Scala · 672 lines · 567 code · 78 blank · 27 comment · 74 complexity · 70d4f066025a7283c2d0d15928890812 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.scaml
  19. import org.fusesource.scalate._
  20. import org.fusesource.scalate.support.{ AbstractCodeGenerator, Code, RenderHelper, Text }
  21. import scala.collection.mutable.LinkedHashMap
  22. import scala.language.implicitConversions
  23. import scala.util.parsing.input.OffsetPosition
  24. /**
  25. * Generates a scala class given a HAML document
  26. *
  27. * @author <a href="http://hiramchirino.com">Hiram Chirino</a>
  28. */
  29. class ScamlCodeGenerator extends AbstractCodeGenerator[Statement] {
  30. override val stratumName = "SCAML"
  31. implicit def textToString(text: Text) = text.value
  32. implicit def textOptionToString(text: Option[Text]): Option[String] = text match {
  33. case None => None
  34. case Some(x) => Some(x.value)
  35. }
  36. protected class SourceBuilder extends AbstractSourceBuilder[Statement] {
  37. val text_buffer = new StringBuffer
  38. var element_level = 0
  39. var pending_newline = false
  40. var suppress_indent = false
  41. var in_html_comment = false
  42. override def current_position = {
  43. if (text_buffer.length == 0) {
  44. super.current_position
  45. } else {
  46. super.current_position + ("$_scalate_$_context << ( " + asString(text_buffer.toString)).length
  47. }
  48. }
  49. def write_indent() = {
  50. if (pending_newline) {
  51. text_buffer.append(ScamlOptions.nl)
  52. pending_newline = false
  53. }
  54. if (suppress_indent) {
  55. suppress_indent = false
  56. } else {
  57. text_buffer.append(indent_string)
  58. }
  59. }
  60. def indent_string() = {
  61. val rc = new StringBuilder
  62. for (i <- 0 until element_level) {
  63. rc.append(ScamlOptions.indent)
  64. }
  65. rc.toString
  66. }
  67. def trim_whitespace() = {
  68. pending_newline = false
  69. suppress_indent = true
  70. }
  71. def write_text(value: String) = {
  72. text_buffer.append(value)
  73. }
  74. def write_nl() = {
  75. pending_newline = true
  76. }
  77. def flush_text() = {
  78. if (pending_newline) {
  79. text_buffer.append(ScamlOptions.nl)
  80. pending_newline = false
  81. }
  82. if (text_buffer.length > 0) {
  83. this << "$_scalate_$_context << ( " + asString(text_buffer.toString) + " );"
  84. text_buffer.setLength(0)
  85. }
  86. }
  87. override def generateInitialImports = {
  88. this << "import _root_.org.fusesource.scalate.support.RenderHelper.{sanitize=>$_scalate_$_sanitize, preserve=>$_scalate_$_preserve, indent=>$_scalate_$_indent, smart_sanitize=>$_scalate_$_smart_sanitize, attributes=>$_scalate_$_attributes}"
  89. }
  90. def generate(statements: List[Statement]): Unit = {
  91. generate_with_flush(statements)
  92. }
  93. def generate_with_flush(statements: List[Statement]): Unit = {
  94. generate_no_flush(statements)
  95. flush_text
  96. }
  97. def generate_no_flush(statements: List[Statement]): Unit = {
  98. var remaining = statements
  99. while (remaining != Nil) {
  100. val fragment = remaining.head
  101. remaining = remaining.drop(1)
  102. fragment match {
  103. case attribute: Attribute =>
  104. this << attribute.pos
  105. generateBindings(List(Binding(attribute.name.value, attribute.className.value, attribute.autoImport, attribute.defaultValue,
  106. classNamePositional = Some(attribute.className), defaultValuePositional = attribute.defaultValue))) {
  107. generate(remaining)
  108. }
  109. remaining = Nil
  110. case _ =>
  111. generate(fragment)
  112. }
  113. }
  114. }
  115. def generate(statement: Statement): Unit = {
  116. statement match {
  117. case s: Newline => {
  118. }
  119. case s: Attribute => {
  120. }
  121. case s: ScamlComment => {
  122. generate(s)
  123. }
  124. case s: TextExpression => {
  125. generateTextExpression(s, true)
  126. }
  127. case s: HtmlComment => {
  128. generate(s)
  129. }
  130. case s: Element => {
  131. generate(s)
  132. }
  133. case s: Executed => {
  134. generate(s)
  135. }
  136. case s: FilterStatement => {
  137. generate(s)
  138. }
  139. case s: Doctype => {
  140. generate(s)
  141. }
  142. }
  143. }
  144. def generate(statement: Doctype): Unit = {
  145. this << statement.pos
  146. write_indent
  147. statement.line.map { _.value } match {
  148. case List("XML") =>
  149. write_text("<?xml version=\"1.0\" encoding=\"utf-8\" ?>")
  150. case List("XML", encoding) =>
  151. write_text("<?xml version=\"1.0\" encoding=\"" + encoding + "\" ?>")
  152. case _ =>
  153. ScamlOptions.format match {
  154. case ScamlOptions.Format.xhtml =>
  155. statement.line.map { _.value } match {
  156. case List("Strict") =>
  157. write_text("""<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">""")
  158. case List("Frameset") =>
  159. write_text("""<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">""")
  160. case List("5") =>
  161. write_text("""<!DOCTYPE html>""")
  162. case List("1.1") =>
  163. write_text("""<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">""")
  164. case List("Basic") =>
  165. write_text("""<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML Basic 1.1//EN" "http://www.w3.org/TR/xhtml-basic/xhtml-basic11.dtd"> """)
  166. case List("Mobile") =>
  167. write_text("""<!DOCTYPE html PUBLIC "-//WAPFORUM//DTD XHTML Mobile 1.2//EN" "http://www.openmobilealliance.org/tech/DTD/xhtml-mobile12.dtd">""")
  168. case _ =>
  169. write_text("""<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">""")
  170. }
  171. case ScamlOptions.Format.html4 =>
  172. statement.line.map { _.value } match {
  173. case List("Strict") =>
  174. write_text("""<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">""")
  175. case List("Frameset") =>
  176. write_text("""<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Frameset//EN" "http://www.w3.org/TR/html4/frameset.dtd">""")
  177. case _ =>
  178. write_text("""<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">""")
  179. }
  180. case ScamlOptions.Format.html5 =>
  181. write_text("""<!DOCTYPE html>""")
  182. }
  183. }
  184. write_nl
  185. }
  186. def generate(statement: FilterStatement): Unit = {
  187. def isEnabled(flag: String) = {
  188. statement.flags.contains(Text(flag))
  189. }
  190. if (isEnabled("&") && isEnabled("!")) {
  191. throw new InvalidSyntaxException("Cannot use both the '&' and '!' filter flags together.", statement.pos)
  192. }
  193. val preserve = isEnabled("~")
  194. val interpolate = isEnabled("&") || isEnabled("!")
  195. val sanitize = interpolate && isEnabled("&")
  196. val content = statement.body.map { _.value }.mkString(ScamlOptions.nl)
  197. val text: TextExpression = if (interpolate) {
  198. val p = new ScamlParser(ScamlParser.UPTO_TYPE_MULTI_LINE)
  199. try {
  200. p.parse(p.literal_text(Some(sanitize)), content)
  201. } catch {
  202. case e: InvalidSyntaxException =>
  203. val pos = statement.body.head.pos.asInstanceOf[OffsetPosition]
  204. throw new InvalidSyntaxException(e.brief, OffsetPosition(pos.source, pos.offset + e.pos.column))
  205. }
  206. } else {
  207. LiteralText(List(Text(content)), Some(sanitize))
  208. }
  209. var prefix = "$_scalate_$_context << ( "
  210. var suffix = ");"
  211. if (ScamlOptions.ugly) {
  212. suppress_indent = true
  213. } else if (preserve) {
  214. prefix += " $_scalate_$_preserve ("
  215. suffix = ") " + suffix
  216. } else {
  217. prefix += "$_scalate_$_indent ( " + asString(indent_string()) + ", "
  218. suffix = ") " + suffix
  219. }
  220. for (f <- statement.filters) {
  221. prefix += "$_scalate_$_context.value ( _root_.org.fusesource.scalate.filter.FilterRequest(" + asString(f) + ", "
  222. suffix = ") ) " + suffix
  223. }
  224. write_indent
  225. flush_text
  226. this << prefix + "$_scalate_$_context.capture { "
  227. indent {
  228. generateTextExpression(text, false)
  229. flush_text
  230. }
  231. this << "} " + suffix
  232. write_nl
  233. }
  234. def generateTextExpression(statement: TextExpression, is_line: Boolean): Unit = {
  235. statement match {
  236. case s: LiteralText => {
  237. if (is_line) {
  238. write_indent
  239. }
  240. var literal = true
  241. for (part <- s.text) {
  242. // alternate between rendering literal and interpolated text
  243. if (literal) {
  244. write_text(part)
  245. literal = false
  246. } else {
  247. flush_text
  248. s.sanitize match {
  249. case None =>
  250. this << "$_scalate_$_context <<< ( " :: part :: " );" :: Nil
  251. case Some(true) =>
  252. this << "$_scalate_$_context.escape( " :: part :: " );" :: Nil
  253. case Some(false) =>
  254. this << "$_scalate_$_context.unescape( " :: part :: " );" :: Nil
  255. }
  256. literal = true
  257. }
  258. }
  259. if (is_line) {
  260. write_nl
  261. }
  262. }
  263. case s: EvaluatedText => {
  264. var prefix = "$_scalate_$_context << ("
  265. var suffix = ");"
  266. if (s.preserve || ScamlOptions.ugly) {
  267. if (s.ugly || ScamlOptions.ugly) {
  268. suppress_indent = true
  269. } else {
  270. prefix += " $_scalate_$_preserve ("
  271. suffix = ") " + suffix
  272. }
  273. } else {
  274. prefix += " $_scalate_$_indent ( " + asString(indent_string()) + ","
  275. suffix = ") " + suffix
  276. }
  277. val method = s.sanitize match {
  278. case Some(true) =>
  279. "valueEscaped"
  280. case Some(false) =>
  281. "valueUnescaped"
  282. case _ =>
  283. "value"
  284. }
  285. prefix += " $_scalate_$_context." + method + "("
  286. suffix = ") " + suffix
  287. if (is_line) {
  288. write_indent
  289. }
  290. flush_text
  291. if (s.body.isEmpty) {
  292. this << prefix
  293. indent {
  294. this << s.code
  295. }
  296. this << suffix
  297. } else {
  298. this << prefix
  299. indent {
  300. this << s.code :: " {" :: Nil
  301. indent {
  302. generate_with_flush(s.body)
  303. }
  304. this << "}"
  305. }
  306. this << suffix
  307. }
  308. if (is_line) {
  309. write_nl
  310. }
  311. }
  312. }
  313. }
  314. def generate(statement: Executed): Unit = {
  315. flush_text
  316. if (statement.body.isEmpty) {
  317. statement.code.foreach {
  318. (line) =>
  319. this << line :: Nil
  320. }
  321. } else {
  322. statement.code.foreach {
  323. (line) =>
  324. if (line ne statement.code.last) {
  325. this << line :: Nil
  326. } else {
  327. this << line :: "{" :: Nil
  328. }
  329. }
  330. indent {
  331. generate_no_flush(statement.body)
  332. flush_text
  333. }
  334. this << "}"
  335. }
  336. }
  337. def generate(statement: HtmlComment): Unit = {
  338. // case class HtmlComment(conditional:Option[String], text:Option[String], body:List[Statement]) extends Statement
  339. var prefix = "<!--"
  340. var suffix = "-->"
  341. if (statement.conditional.isDefined) {
  342. prefix = "<!--[" + statement.conditional.get + "]>"
  343. suffix = "<![endif]-->"
  344. }
  345. // To support comment within comment blocks.
  346. if (in_html_comment) {
  347. prefix = ""
  348. suffix = ""
  349. } else {
  350. in_html_comment = true
  351. }
  352. statement match {
  353. case HtmlComment(_, text, List()) => {
  354. write_indent
  355. this << statement.pos
  356. write_text(prefix + " ")
  357. if (text.isDefined) {
  358. this << text.get.pos
  359. write_text(text.get.trim)
  360. }
  361. write_text(" " + suffix)
  362. write_nl
  363. }
  364. case HtmlComment(_, None, list) => {
  365. write_indent
  366. this << statement.pos
  367. write_text(prefix)
  368. write_nl
  369. element_level += 1
  370. generate_no_flush(list)
  371. element_level -= 1
  372. write_indent
  373. write_text(suffix)
  374. write_nl
  375. }
  376. case _ => throw new InvalidSyntaxException("Illegal nesting: content can't be both given on the same line as html comment and nested within it", statement.pos);
  377. }
  378. if (prefix.length != 0) {
  379. in_html_comment = false
  380. }
  381. }
  382. def generate(statement: ScamlComment): Unit = {
  383. this << statement.pos
  384. statement match {
  385. case ScamlComment(text, List()) => {
  386. this << "//" :: text.getOrElse("") :: Nil
  387. }
  388. case ScamlComment(text, list) => {
  389. this << "/*" :: text.getOrElse("") :: Nil
  390. list.foreach(x => {
  391. this << " * " :: x :: Nil
  392. })
  393. this << " */"
  394. }
  395. }
  396. }
  397. def isAutoClosed(statement: Element) = {
  398. statement.text == None && statement.body.isEmpty &&
  399. statement.tag.isDefined && (ScamlOptions.autoclose == null || ScamlOptions.autoclose.contains(statement.tag.get.value))
  400. }
  401. def generate(statement: Element): Unit = {
  402. val tag = statement.tag.getOrElse("div")
  403. if (statement.text.isDefined && !statement.body.isEmpty) {
  404. throw new InvalidSyntaxException("Illegal nesting: content can't be given on the same line as html element or nested within it if the tag is closed", statement.pos)
  405. }
  406. def write_start_tag = {
  407. write_text("<" + tag)
  408. write_attributes(statement.attributes)
  409. if (statement.close || isAutoClosed(statement)) {
  410. write_text("/>")
  411. } else {
  412. write_text(">")
  413. }
  414. }
  415. def write_end_tag = {
  416. if (statement.close || isAutoClosed(statement)) {
  417. write_text("")
  418. } else {
  419. write_text("</" + tag + ">")
  420. }
  421. }
  422. statement.trim match {
  423. case Some(Trim.Outer) => {
  424. }
  425. case Some(Trim.Inner) => {}
  426. case Some(Trim.Both) => {}
  427. case _ => {}
  428. }
  429. def outer_trim = statement.trim match {
  430. case Some(Trim.Outer) => { trim_whitespace; true }
  431. case Some(Trim.Both) => { trim_whitespace; true }
  432. case _ => { false }
  433. }
  434. def inner_trim = statement.trim match {
  435. case Some(Trim.Inner) => { trim_whitespace; true }
  436. case Some(Trim.Both) => { trim_whitespace; true }
  437. case _ => { false }
  438. }
  439. outer_trim
  440. this << statement.pos
  441. write_indent
  442. write_start_tag
  443. statement match {
  444. case Element(_, _, text, List(), _, _) => {
  445. generateTextExpression(text.getOrElse(LiteralText(List(Text("")), Some(false))), false)
  446. write_end_tag
  447. write_nl
  448. outer_trim
  449. }
  450. case Element(_, _, None, list, _, _) => {
  451. write_nl
  452. if (!inner_trim) {
  453. element_level += 1
  454. }
  455. generate_no_flush(list)
  456. if (!inner_trim) {
  457. element_level -= 1
  458. }
  459. write_indent
  460. write_end_tag
  461. write_nl
  462. outer_trim
  463. }
  464. case _ => throw new InvalidSyntaxException("Illegal nesting: content can't be both given on the same line as html element and nested within it", statement.pos);
  465. }
  466. }
  467. def write_attributes(entries: List[(Any, Any)]) = {
  468. // Check to see if it's a dynamic attribute list
  469. var dynamic = false
  470. def check(n: Any): Unit = n match {
  471. case x: EvaluatedText =>
  472. dynamic = true
  473. case x: LiteralText =>
  474. if (x.text.length > 1) {
  475. dynamic = true
  476. }
  477. case _ =>
  478. }
  479. for ((k, v) <- entries) {
  480. check(k)
  481. check(v)
  482. }
  483. if (dynamic) {
  484. def write_expression(expression: Any) = {
  485. expression match {
  486. case s: String =>
  487. this << asString(s)
  488. case s: Text =>
  489. this << s.pos
  490. this << asString(s)
  491. case s: LiteralText =>
  492. this << s.pos
  493. var literal = true
  494. val parts = s.text.map { part =>
  495. // alternate between rendering literal and interpolated expression
  496. if (literal) {
  497. literal = !literal
  498. asString(part) :: Nil
  499. } else {
  500. literal = !literal
  501. List[AnyRef]("$_scalate_$_context.value(", part, ", false)")
  502. }
  503. }
  504. this << parts.foldRight(List[AnyRef]()) {
  505. case (prev, sum) =>
  506. sum match {
  507. case List() => prev
  508. case _ => prev ::: " + " :: sum
  509. }
  510. }
  511. flush_text
  512. case s: EvaluatedText =>
  513. if (s.body.isEmpty) {
  514. this << s.code :: Nil
  515. } else {
  516. this << s.code :: " {" :: Nil
  517. indent {
  518. generate_with_flush(s.body)
  519. }
  520. this << "} "
  521. }
  522. case _ => throw new UnsupportedOperationException("don't know how to eval: " + expression);
  523. }
  524. }
  525. flush_text
  526. this << "$_scalate_$_context << $_scalate_$_attributes( $_scalate_$_context, List( ("
  527. indent {
  528. var first = true
  529. entries.foreach {
  530. (entry) =>
  531. if (!first) {
  532. this << "), ("
  533. }
  534. first = false
  535. indent {
  536. write_expression(entry._1)
  537. }
  538. this << ","
  539. indent {
  540. write_expression(entry._2)
  541. }
  542. }
  543. }
  544. this << ") ) )"
  545. } else {
  546. def value_of(value: Any): Text = {
  547. value match {
  548. case LiteralText(text, _) => text.head
  549. case s: Text => s
  550. case _ => throw new UnsupportedOperationException("don't know how to deal with: " + value);
  551. }
  552. }
  553. val (entries_class, tmp) = entries.partition { x => { x._1 match { case "class" => true; case _ => false } } }
  554. val (entries_id, entries_rest) = tmp.partition { x => { x._1 match { case "id" => true; case _ => false } } }
  555. val map = LinkedHashMap[Text, Text]()
  556. if (!entries_id.isEmpty) {
  557. map += Text("id") -> value_of(entries_id.last._2)
  558. }
  559. if (!entries_class.isEmpty) {
  560. var value: Option[Text] = None
  561. value = entries_class.foldLeft(value) {
  562. (rc, x) =>
  563. rc match {
  564. case None => Some(value_of(x._2))
  565. case Some(y) => Some(y + " " + value_of(x._2))
  566. }
  567. }
  568. map += Text("class") -> value.get
  569. }
  570. entries_rest.foreach { me => map += value_of(me._1) -> value_of(me._2) }
  571. if (!map.isEmpty) {
  572. map.foreach {
  573. case (name, value) =>
  574. write_text(" ")
  575. this << name.pos
  576. write_text(name)
  577. write_text("=\"")
  578. this << value.pos
  579. write_text(RenderHelper.sanitize(value))
  580. write_text("\"")
  581. }
  582. }
  583. }
  584. }
  585. }
  586. override def generate(engine: TemplateEngine, source: TemplateSource, bindings: Iterable[Binding]): Code = {
  587. val uri = source.uri
  588. val hamlSource = source.text
  589. val statements = (new ScamlParser).parse(hamlSource)
  590. val builder = new SourceBuilder()
  591. builder.generate(engine, source, bindings, statements)
  592. Code(source.className, builder.code, Set(uri), builder.positions)
  593. }
  594. }