PageRenderTime 55ms CodeModel.GetById 14ms app.highlight 34ms RepoModel.GetById 2ms app.codeStats 0ms

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