temple 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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