temple 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/temple/engine.rb CHANGED
@@ -1,13 +1,26 @@
1
1
  module Temple
2
- # An engine is simply a chain of compilers (that includes both parsers,
3
- # filters and generators).
2
+ # An engine is simply a chain of compilers (that often includes a parser,
3
+ # some filters and a generator).
4
4
  #
5
- # class ERB < Temple::Engine
6
- # parser :ERB # shortcut for use Temple::Parsers::ERB
7
- # filter :Escapable # use Temple::Filters::Escapable
8
- # filter :DynamicInliner # use Temple::Filters::DynamicInliner
9
- # generator :ArrayBuffer # use Temple::Core::ArrayBuffer
10
- # end
5
+ # class MyEngine < Temple::Engine
6
+ # # First run MyParser, passing the :strict option
7
+ # use MyParser, :strict
8
+ #
9
+ # # Then a custom filter
10
+ # use MyFilter
11
+ #
12
+ # # Then some general optimizations filters
13
+ # filter :MultiFlattener
14
+ # filter :StaticMerger
15
+ # filter :DynamicInliner
16
+ #
17
+ # # Finally the generator
18
+ # generator :ArrayBuffer, :buffer
19
+ # end
20
+ #
21
+ # engine = MyEngine.new(:strict => "For MyParser")
22
+ # engine.compile(something)
23
+ #
11
24
  class Engine
12
25
  def self.filters
13
26
  @filters ||= []
@@ -17,21 +30,30 @@ module Temple
17
30
  filters << [filter, args, blk]
18
31
  end
19
32
 
33
+ # Shortcut for <tt>use Temple::Parsers::parser</tt>
20
34
  def self.parser(parser, *args, &blk)
21
35
  use(Temple::Parsers.const_get(parser), *args, &blk)
22
36
  end
23
37
 
38
+ # Shortcut for <tt>use Temple::Filters::parser</tt>
24
39
  def self.filter(filter, *args, &blk)
25
40
  use(Temple::Filters.const_get(filter), *args, &blk)
26
41
  end
27
42
 
43
+ # Shortcut for <tt>use Temple::Generator::parser</tt>
28
44
  def self.generator(compiler, *args, &blk)
29
45
  use(Temple::Core.const_get(compiler), *args, &blk)
30
46
  end
31
47
 
32
48
  def initialize(options = {})
33
49
  @chain = self.class.filters.map do |filter, args, blk|
34
- filter.new(*args, &blk)
50
+ opt = args.last.is_a?(Hash) ? args.pop : {}
51
+ opt = args.inject(opt) do |memo, ele|
52
+ memo[ele] = options[ele] if options.has_key?(ele)
53
+ memo
54
+ end
55
+
56
+ filter.new(opt, &blk)
35
57
  end
36
58
  end
37
59
 
@@ -0,0 +1,93 @@
1
+ require 'erb'
2
+
3
+ module Temple
4
+ module Engines
5
+ # An engine which works in-place for ERB:
6
+ #
7
+ # require 'temple'
8
+ #
9
+ # template = Temple::Engine::ERB.new("<%= 1 + 1 %>")
10
+ # template.result # => "2"
11
+ class ERB < ::ERB
12
+ OriginalERB = ::ERB
13
+ Optimizers = [Filters::StaticMerger.new, Filters::DynamicInliner.new]
14
+
15
+ # The optional _filename_ argument passed to Kernel#eval when the ERB
16
+ # code is run
17
+ attr_accessor :filename
18
+
19
+ # The Ruby code generated by ERB
20
+ attr_reader :src
21
+
22
+ # The sexp generated by Temple
23
+ attr_reader :sexp
24
+
25
+ # The optimized sexp generated by Temple
26
+ attr_reader :optimized_sexp
27
+
28
+ # Sets the ERB constant to Temple::Engine::ERB
29
+ #
30
+ # Example:
31
+ #
32
+ # require 'temple'
33
+ # Temple::Engine::ERB.rock!
34
+ # ERB == Temple::Engine::ERB
35
+ def self.rock!
36
+ Object.send(:remove_const, :ERB)
37
+ Object.send(:const_set, :ERB, self)
38
+ end
39
+
40
+ # Sets the ERB constant back to regular ERB
41
+ #
42
+ # Example:
43
+ #
44
+ # require 'temple'
45
+ # original_erb = ERB
46
+ # Temple::Engine::ERB.rock!
47
+ # ERB.suck!
48
+ # ERB == original_erb
49
+
50
+ def self.suck!
51
+ Object.send(:remove_const, :ERB)
52
+ Object.send(:const_set, :ERB, OriginalERB)
53
+ end
54
+
55
+ def initialize(str, safe_level = nil, trim_mode = nil, eoutvar = '_erbout', options = {})
56
+ @safe_level = safe_level
57
+ @trim_mode = trim_mode
58
+ @parser = Parsers::ERB.new(:trim_mode => @trim_mode)
59
+
60
+ @generator = options[:generator] || Core::ArrayBuffer
61
+
62
+ if @generator.is_a?(Class)
63
+ @generator = @generator.new(:buffer => eoutvar)
64
+ end
65
+
66
+ @sexp = @parser.compile(str)
67
+ @optimized_sexp = Optimizers.inject(@sexp) { |m, e| e.compile(m) }
68
+ @src = @generator.compile(@optimized_sexp)
69
+
70
+ if str.respond_to?(:encoding)
71
+ @enc = detect_magic_comment(str) || str.encoding
72
+ @src.insert(0, "#coding:#{@enc}\n")
73
+ @src << ".force_encoding(__ENCODING__)"
74
+ end
75
+ end
76
+
77
+ def percent?
78
+ @trim_mode.is_a?(String) and @trim_mode.include?("%")
79
+ end
80
+
81
+ def detect_magic_comment(s)
82
+ if /\A<%#(.*)%>/ =~ s or (percent? and /\A%#(.*)/ =~ s)
83
+ comment = $1
84
+ comment = $1 if comment[/-\*-\s*(.*?)\s*-*-$/]
85
+ if %r"coding\s*[=:]\s*([[:alnum:]\-_]+)" =~ comment
86
+ enc = $1.sub(/-(?:mac|dos|unix)/i, '')
87
+ enc = Encoding.find(enc)
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
@@ -2,6 +2,10 @@ module Temple
2
2
  module Filters
3
3
  # Inlines several static/dynamic into a single dynamic.
4
4
  class DynamicInliner
5
+ def initialize(options = {})
6
+ @options = {}
7
+ end
8
+
5
9
  def compile(exp)
6
10
  exp.first == :multi ? on_multi(*exp[1..-1]) : exp
7
11
  end
@@ -9,21 +13,37 @@ module Temple
9
13
  def on_multi(*exps)
10
14
  res = [:multi]
11
15
  curr = nil
12
- prev = nil
16
+ prev = []
13
17
  state = :looking
14
18
 
15
19
  # We add a noop because we need to do some cleanup at the end too.
16
20
  (exps + [:noop]).each do |exp|
17
21
  head, arg = exp
18
-
19
- if head == :dynamic || head == :static
22
+
23
+ case head
24
+ when :newline
25
+ case state
26
+ when :looking
27
+ # We haven't found any static/dynamic, so let's just add it
28
+ res << exp
29
+ when :single, :several
30
+ # We've found something, so let's make sure the generated
31
+ # dynamic contains a newline by escaping a newline and
32
+ # starting a new string:
33
+ #
34
+ # "Hello "\
35
+ # "#{@world}"
36
+ prev << exp
37
+ curr[1] << "\"\\\n\""
38
+ end
39
+ when :dynamic, :static
20
40
  case state
21
41
  when :looking
22
42
  # Found a single static/dynamic. We don't want to turn this
23
43
  # into a dynamic yet. Instead we store it, and if we find
24
44
  # another one, we add both then.
25
45
  state = :single
26
- prev = exp
46
+ prev = [exp]
27
47
  curr = [:dynamic, '"' + send(head, arg)]
28
48
  when :single
29
49
  # Yes! We found another one. Append the content to the current
@@ -39,7 +59,7 @@ module Temple
39
59
  # We need to add the closing quote.
40
60
  curr[1] << '"' unless state == :looking
41
61
  # If we found a single exp last time, let's add it.
42
- res << prev if state == :single
62
+ res.concat(prev) if state == :single
43
63
  # Compile the current exp (unless it's the noop)
44
64
  res << compile(exp) unless head == :noop
45
65
  # Now we're looking for more!
@@ -51,7 +71,7 @@ module Temple
51
71
  end
52
72
 
53
73
  def static(str)
54
- str.inspect[1..-2]
74
+ Generator.to_ruby(str)[1..-2]
55
75
  end
56
76
 
57
77
  def dynamic(str)
@@ -1,17 +1,24 @@
1
+ require 'cgi'
2
+
1
3
  module Temple
2
4
  module Filters
3
5
  class Escapable
4
6
  def initialize(options = {})
5
- @escaper = options[:escaper] || 'CGI.escapeHTML(%s.to_s)'
7
+ @escaper = options[:escaper] || 'CGI.escapeHTML((%s).to_s)'
6
8
  end
7
9
 
8
10
  def compile(exp)
9
11
  return exp if !exp.is_a?(Enumerable) || exp.is_a?(String)
10
12
 
11
- if exp[0] == :static && is_escape?(exp[1])
12
- [:static, eval(@escaper % exp[1][1].inspect)]
13
- elsif is_escape?(exp)
14
- @escaper % exp[1]
13
+ if is_escape?(exp)
14
+ case exp[1][0]
15
+ when :static
16
+ [:static, eval(@escaper % exp[1][1].inspect)]
17
+ when :dynamic, :block
18
+ [exp[1][0], @escaper % exp[1][1]]
19
+ else
20
+ raise "Escapable can only handle :static, :dynamic and :block for the moment."
21
+ end
15
22
  else
16
23
  exp.map { |e| compile(e) }
17
24
  end
@@ -0,0 +1,27 @@
1
+ module Temple
2
+ module Filters
3
+ class MultiFlattener
4
+ def initialize(options = {})
5
+ @options = {}
6
+ end
7
+
8
+ def compile(exp)
9
+ return exp unless exp.first == :multi
10
+ # If the multi contains a single element, just return the element
11
+ return compile(exp[1]) if exp.length == 2
12
+ result = [:multi]
13
+
14
+ exp[1..-1].each do |e|
15
+ e = compile(e)
16
+ if e.first == :multi
17
+ result.concat(e[1..-1])
18
+ else
19
+ result << e
20
+ end
21
+ end
22
+
23
+ result
24
+ end
25
+ end
26
+ end
27
+ end
@@ -11,6 +11,10 @@ module Temple
11
11
  # [:multi,
12
12
  # [:static, "Hello World!"]]
13
13
  class StaticMerger
14
+ def initialize(options = {})
15
+ @options = {}
16
+ end
17
+
14
18
  def compile(exp)
15
19
  exp.first == :multi ? on_multi(*exp[1..-1]) : exp
16
20
  end
@@ -30,7 +34,7 @@ module Temple
30
34
  end
31
35
  else
32
36
  res << compile(exp)
33
- state = :looking
37
+ state = :looking unless exp.first == :newline
34
38
  end
35
39
  end
36
40
 
@@ -4,29 +4,83 @@ module Temple
4
4
  :buffer => "_buf"
5
5
  }
6
6
 
7
+ CONCATABLE = [:static, :dynamic]
8
+
7
9
  def initialize(options = {})
8
10
  @options = DEFAULT_OPTIONS.merge(options)
11
+ @in_multi = false
9
12
  end
10
13
 
11
14
  def compile(exp)
12
- preamble + compile_part(exp) + postamble
13
- end
14
-
15
- def compile_part(exp)
16
- send("on_#{exp.first}", *exp[1..-1])
15
+ res = send("on_#{exp.first}", *exp[1..-1])
16
+
17
+ if @in_multi && CONCATABLE.include?(exp.first)
18
+ concat(res)
19
+ else
20
+ res
21
+ end
17
22
  end
18
23
 
19
24
  def buffer(str = '')
20
25
  @options[:buffer] + str
21
26
  end
22
27
 
28
+ def self.to_ruby(str)
29
+ str.inspect.gsub(/(\\r)?\\n/m) do |str|
30
+ if $`[-1] == ?\\
31
+ str
32
+ elsif $1
33
+ "\\n"
34
+ else
35
+ "\n"
36
+ end
37
+ end
38
+ end
39
+
40
+ def to_ruby(str)
41
+ Generator.to_ruby(str)
42
+ end
43
+
23
44
  # Sensible defaults
24
45
 
25
46
  def preamble; '' end
26
47
  def postamble; '' end
48
+ def concat(s) buffer " << (#{s})" end
27
49
 
28
50
  def on_multi(*exp)
29
- exp.map { |e| compile_part(e) }.join
51
+ if @in_multi
52
+ exp.map { |e| compile(e) }
53
+ else
54
+ @in_multi = true
55
+ content = exp.map { |e| compile(e) }
56
+ content = [preamble, content, postamble].flatten
57
+ @in_multi = false
58
+ content
59
+ end.join(" ; ")
60
+ end
61
+
62
+ def on_newline
63
+ "\n"
64
+ end
65
+
66
+ def on_capture(name, block)
67
+ unless @in_multi
68
+ # We always need the preamble/postamble in capture.
69
+ return compile([:multi, [:capture, name, block]])
70
+ end
71
+
72
+ @in_multi = false
73
+ prev_buffer, @options[:buffer] = @options[:buffer], name.to_s
74
+ content = compile(block)
75
+ @in_multi = true
76
+
77
+ if CONCATABLE.include?(block.first)
78
+ "#{name} = #{content}"
79
+ else
80
+ content
81
+ end
82
+ ensure
83
+ @options[:buffer] = prev_buffer
30
84
  end
31
85
  end
32
86
  end
@@ -1,26 +1,74 @@
1
+ require 'erb'
2
+
1
3
  module Temple
2
4
  module Parsers
3
- # A dead simple ERB parser which compiles directly to Core.
4
- # It's crappy, but it works for now.
5
+ # Parses ERB exactly the same way as erb.rb.
5
6
  class ERB
7
+ Compiler = ::ERB::Compiler
8
+
9
+ def initialize(options = {})
10
+ @compiler = Compiler.new(options[:trim_mode])
11
+ end
12
+
6
13
  def compile(src)
14
+ if src.respond_to?(:encoding) && src.encoding.dummy?
15
+ raise ArgumentError, "#{src.encoding} is not ASCII compatible"
16
+ end
17
+
7
18
  result = [:multi]
8
- while src =~ /<%(.*?)%>/
9
- result << [:static, $`]
10
- text = $1[1..-1].strip
11
- case $1[0]
12
- when ?#
13
- next
14
- when ?=
15
- text = [:escape, text] if $1[1] == ?=
16
- head = :dynamic
19
+
20
+ content = ''
21
+ scanner = @compiler.make_scanner(src)
22
+ scanner.scan do |token|
23
+ next if token.nil?
24
+ next if token == ''
25
+ if scanner.stag.nil?
26
+ case token
27
+ when Compiler::PercentLine
28
+ result << [:static, content] if content.size > 0
29
+ content = ''
30
+ result << [:block, token.to_s.strip]
31
+ result << [:newline]
32
+ when :cr
33
+ result << [:newline]
34
+ when '<%', '<%=', '<%#'
35
+ scanner.stag = token
36
+ when "\n"
37
+ content << "\n"
38
+ result << [:static, content]
39
+ content = ''
40
+ when '<%%'
41
+ result << [:static, '<%']
42
+ else
43
+ result << [:static, token]
44
+ end
17
45
  else
18
- head = :block
46
+ case token
47
+ when '%>'
48
+ case scanner.stag
49
+ when '<%'
50
+ if content[-1] == ?\n
51
+ content.chop!
52
+ result << [:block, content]
53
+ result << [:newline]
54
+ else
55
+ result << [:block, content]
56
+ end
57
+ when '<%='
58
+ result << [:dynamic, content]
59
+ when '<%#'
60
+ # nothing
61
+ end
62
+ scanner.stag = nil
63
+ content = ''
64
+ when '%%>'
65
+ content << '%>'
66
+ else
67
+ content << token
68
+ end
19
69
  end
20
- result << [head, text]
21
- src = $'
22
70
  end
23
- result << [:static, src]
71
+
24
72
  result
25
73
  end
26
74
  end