cerubis 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (75) hide show
  1. data/.gitignore +10 -0
  2. data/.travis.yml +14 -0
  3. data/Gemfile +15 -0
  4. data/README.md +111 -0
  5. data/Rakefile +15 -0
  6. data/TODO.md +31 -0
  7. data/cerubis.gemspec +22 -0
  8. data/examples/blocks.rb +20 -0
  9. data/examples/full/blocks/script_block.rb +8 -0
  10. data/examples/full/config.rb +14 -0
  11. data/examples/full/example.rb +25 -0
  12. data/examples/full/helpers/html_helpers.rb +13 -0
  13. data/examples/full/helpers/include_helper.rb +15 -0
  14. data/examples/full/models/page.rb +17 -0
  15. data/examples/full/models/site.rb +13 -0
  16. data/examples/full/templates/_footer.cerubis +1 -0
  17. data/examples/full/templates/_header.cerubis +1 -0
  18. data/examples/full/templates/main.cerubis +31 -0
  19. data/examples/helpers.rb +22 -0
  20. data/examples/html.rb +18 -0
  21. data/examples/variables.rb +10 -0
  22. data/lib/cerubis.rb +55 -0
  23. data/lib/cerubis/block.rb +27 -0
  24. data/lib/cerubis/block_node.rb +37 -0
  25. data/lib/cerubis/blocks/if.rb +8 -0
  26. data/lib/cerubis/blocks/loop.rb +20 -0
  27. data/lib/cerubis/blocks/unless.rb +9 -0
  28. data/lib/cerubis/condition.rb +59 -0
  29. data/lib/cerubis/context.rb +30 -0
  30. data/lib/cerubis/helper.rb +12 -0
  31. data/lib/cerubis/matcher.rb +19 -0
  32. data/lib/cerubis/method.rb +19 -0
  33. data/lib/cerubis/node.rb +27 -0
  34. data/lib/cerubis/objects/array.rb +4 -0
  35. data/lib/cerubis/objects/fixnum.rb +4 -0
  36. data/lib/cerubis/objects/float.rb +4 -0
  37. data/lib/cerubis/objects/hash.rb +4 -0
  38. data/lib/cerubis/objects/string.rb +4 -0
  39. data/lib/cerubis/parser.rb +125 -0
  40. data/lib/cerubis/syntax_error.rb +4 -0
  41. data/lib/cerubis/template.rb +21 -0
  42. data/lib/cerubis/text_node.rb +10 -0
  43. data/lib/cerubis/variable_replacement.rb +34 -0
  44. data/lib/cerubis/version.rb +3 -0
  45. data/test/all.rb +3 -0
  46. data/test/cerubis/block_node_test.rb +36 -0
  47. data/test/cerubis/blocks/if_test.rb +24 -0
  48. data/test/cerubis/blocks/loop_test.rb +25 -0
  49. data/test/cerubis/blocks/unless_test.rb +25 -0
  50. data/test/cerubis/condition_test.rb +142 -0
  51. data/test/cerubis/context_test.rb +33 -0
  52. data/test/cerubis/helper_test.rb +17 -0
  53. data/test/cerubis/matcher_test.rb +20 -0
  54. data/test/cerubis/method_test.rb +60 -0
  55. data/test/cerubis/parser_test.rb +48 -0
  56. data/test/cerubis/template_test.rb +38 -0
  57. data/test/cerubis/text_node_test.rb +16 -0
  58. data/test/cerubis_test.rb +31 -0
  59. data/test/matchers/test_block_name.rb +25 -0
  60. data/test/matchers/test_close_block.rb +25 -0
  61. data/test/matchers/test_conditions.rb +21 -0
  62. data/test/matchers/test_helpers.rb +21 -0
  63. data/test/matchers/test_object_method.rb +37 -0
  64. data/test/matchers/test_open_block.rb +57 -0
  65. data/test/matchers/test_operators.rb +29 -0
  66. data/test/matchers/test_variable.rb +37 -0
  67. data/test/methods/test_array_methods.rb +21 -0
  68. data/test/methods/test_fixnum_methods.rb +6 -0
  69. data/test/methods/test_float_methods.rb +6 -0
  70. data/test/methods/test_hash_methods.rb +11 -0
  71. data/test/methods/test_string_methods.rb +11 -0
  72. data/test/nodes/test_node_defaults.rb +30 -0
  73. data/test/rendered_test.rb +159 -0
  74. data/test/test_helper.rb +34 -0
  75. metadata +149 -0
@@ -0,0 +1,10 @@
1
+ require 'bundler/setup'
2
+ require 'cerubis'
3
+
4
+ content = <<-STR
5
+ Welcome {{name}}!
6
+ You are {{age}} years old today
7
+ STR
8
+
9
+ template = Cerubis.render(content, name: 'Dane Harrigan', age: 26)
10
+ puts template.to_html
data/lib/cerubis.rb ADDED
@@ -0,0 +1,55 @@
1
+ require 'cerubis/variable_replacement'
2
+ require 'cerubis/block'
3
+ require 'cerubis/blocks/if'
4
+ require 'cerubis/blocks/unless'
5
+ require 'cerubis/blocks/loop'
6
+
7
+ class Cerubis
8
+ autoload :Node, 'cerubis/node'
9
+ autoload :TextNode, 'cerubis/text_node'
10
+ autoload :BlockNode, 'cerubis/block_node'
11
+ autoload :Matcher, 'cerubis/matcher'
12
+ autoload :Template, 'cerubis/template'
13
+ autoload :Parser, 'cerubis/parser'
14
+ autoload :Condition, 'cerubis/condition'
15
+ autoload :Context, 'cerubis/context'
16
+ autoload :Method, 'cerubis/method'
17
+ autoload :Helper, 'cerubis/helper'
18
+ autoload :SyntaxError, 'cerubis/syntax_error'
19
+
20
+ def self.register_block(name, klass)
21
+ blocks[name] = klass
22
+ end
23
+
24
+ def self.register_helper(*mods)
25
+ mod = mods.pop
26
+ mods.each { |key| helpers[key] = mod }
27
+ end
28
+
29
+ def self.blocks
30
+ @blocks ||= {}
31
+ end
32
+
33
+ def self.helpers
34
+ @helpers ||= {}
35
+ end
36
+
37
+ def self.render(template, context={})
38
+ new.render(template, context)
39
+ end
40
+
41
+ def render(template, context={})
42
+ Template.new(template, context)
43
+ end
44
+ end
45
+
46
+ # Patching Ruby core objects
47
+ require 'cerubis/objects/array'
48
+ require 'cerubis/objects/float'
49
+ require 'cerubis/objects/fixnum'
50
+ require 'cerubis/objects/hash'
51
+ require 'cerubis/objects/string'
52
+
53
+ Cerubis.register_block :if, Cerubis::Blocks::If
54
+ Cerubis.register_block :unless, Cerubis::Blocks::Unless
55
+ Cerubis.register_block :loop, Cerubis::Blocks::Loop
@@ -0,0 +1,27 @@
1
+ class Cerubis
2
+ module Block
3
+ def self.included(base)
4
+ base.send(:attr, :condition)
5
+ base.send(:attr, :node)
6
+ end
7
+
8
+ def initialize(options)
9
+ @node = options[:node]
10
+ @condition = Condition.new(options[:condition], :context => node.context, :type => options[:type])
11
+ end
12
+
13
+ def true?
14
+ condition.true?
15
+ end
16
+
17
+ def render
18
+ return unless true?
19
+ replace_variables(yield)
20
+ end
21
+
22
+ private
23
+ def context
24
+ node.context
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,37 @@
1
+ class Cerubis
2
+ class BlockNode
3
+ include Node
4
+ attr_accessor :block
5
+
6
+ def render
7
+ @render ||= begin
8
+ define_node!
9
+ parse!
10
+ block.render { pre_render }
11
+ end
12
+ end
13
+
14
+ def pre_render
15
+ return children.map(&:render).join if children?
16
+ content
17
+ end
18
+
19
+ private
20
+ def parse!
21
+ self.children = Parser.new(content, :parent => self).nodes
22
+ end
23
+
24
+ def define_node!
25
+ open_block = content.match(/^([\s\t]*)(#{Matcher::OpenBlock})/m)
26
+ close_block = Matcher::CloseBlockPlaceholder.sub('block_name', open_block[3])
27
+ block_name = open_block[3].to_sym
28
+ condition_str = open_block[4]
29
+
30
+ content.sub!(/^#{open_block[0]}/, open_block[1])
31
+ content.sub!(/#{close_block}+\Z/,'')
32
+
33
+ options = { :context => context, :node => self, :condition => condition_str, :type => block_name }
34
+ self.block = Cerubis.blocks[block_name].new(options)
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,8 @@
1
+ class Cerubis
2
+ module Blocks
3
+ class If
4
+ include VariableReplacement
5
+ include Block
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,20 @@
1
+ class Cerubis
2
+ module Blocks
3
+ class Loop
4
+ include VariableReplacement
5
+ include Block
6
+
7
+ def render
8
+ return unless true?
9
+ collection = condition.context_objects[1]
10
+ item_key = condition.parsed_content[0].to_sym
11
+ loop_context = context.dup
12
+
13
+ collection.map do |item|
14
+ loop_context[item_key] = item
15
+ replace_variables(yield, loop_context)
16
+ end.join
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,9 @@
1
+ class Cerubis
2
+ module Blocks
3
+ class Unless < If
4
+ def true?
5
+ !super
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,59 @@
1
+ class Cerubis
2
+ class Condition
3
+ attr :content
4
+ attr :type
5
+ attr :context
6
+ attr :context_objects
7
+ attr :parsed_content
8
+
9
+ def initialize(content, options={})
10
+ @content = content.to_s.strip
11
+ @context = options[:context]
12
+ @type = options[:type]
13
+ @context_objects = []
14
+
15
+ define_condition!
16
+ end
17
+
18
+ def true?
19
+ if @parsed_content.size == 3
20
+ validate_object_and_operator
21
+ else
22
+ validate_object
23
+ end
24
+ end
25
+
26
+ private
27
+ def define_condition!
28
+ match = content.match Matcher::Conditions
29
+ @parsed_content = match ? [match[1], match[6], match[7]].compact : []
30
+ end
31
+
32
+ def validate_object
33
+ @context_objects << context.get(@parsed_content[0])
34
+ @context_objects[0]
35
+ end
36
+
37
+ def validate_object_and_operator
38
+ operator = @parsed_content[1].to_sym
39
+ obj = context.get(@parsed_content[0])
40
+ obj_compared = context.get(@parsed_content[2])
41
+
42
+ @context_objects << obj
43
+ @context_objects << obj_compared
44
+
45
+ case operator
46
+ when :== then (obj == obj_compared)
47
+ when :=== then (obj === obj_compared)
48
+ when :'!=' then (obj != obj_compared)
49
+ when :< then (obj < obj_compared)
50
+ when :> then (obj > obj_compared)
51
+ when :<= then (obj <= obj_compared)
52
+ when :>= then (obj >= obj_compared)
53
+ when :in
54
+ raise SyntaxError, 'Invalid block conditions' unless type == :loop
55
+ obj_compared && !obj_compared.empty?
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,30 @@
1
+ class Cerubis
2
+ class Context < Hash
3
+ def initialize(hash={})
4
+ super(nil)
5
+ hash.each { |key, value| self[key] = value }
6
+ end
7
+
8
+ def get(key)
9
+ case key
10
+ when /^true$/ then true
11
+ when /^false$/ then false
12
+ when /^[0-9]+$/ then Integer(key)
13
+ when /^[0-9]+\.[0-9]+$/ then Float(key)
14
+ when /^'.*'$/ then key[1..-2]
15
+ else
16
+ object_methods = key.split('.')
17
+ object = self[object_methods.shift.to_sym]
18
+ while meth = object_methods.shift do
19
+ if object.respond_to?(:cerubis_respond_to?) && object.cerubis_respond_to?(meth.to_sym)
20
+ object = object.send(meth)
21
+ else
22
+ object = nil
23
+ end
24
+ end
25
+
26
+ object
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,12 @@
1
+ class Cerubis
2
+ class Helper
3
+ def initialize(context)
4
+ @context = context
5
+ end
6
+
7
+ private
8
+ def context
9
+ @context
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,19 @@
1
+ class Cerubis
2
+ module Matcher
3
+ OpenTag = '{{'
4
+ CloseTag = '}}'
5
+ BlockName = '[a-z_]+'
6
+ Method = '[a-z_0-9]+'
7
+ ObjectMethod = /\'([^\']*)\'|[a-z0-9_]+((\.#{Method}\??)+)?/
8
+ Operators = [:==, :===, :'!=', :'!==', :<, :>, :<=, :>=, :in]
9
+ Conditions = /(#{ObjectMethod})(\s*(#{Operators.join('|')})\s*(#{ObjectMethod}))?/
10
+ OpenBlockStr = "#{OpenTag}\#(#{BlockName})(\s+#{Conditions})?#{CloseTag}"
11
+ CloseBlockPlaceholder = "#{OpenTag}\/(block_name)#{CloseTag}"
12
+ CloseBlockStr = CloseBlockPlaceholder.sub('block_name', BlockName)
13
+ OpenBlock = /#{OpenBlockStr}/
14
+ CloseBlock = /#{CloseBlockStr}/
15
+ Helpers = /(#{Method})\s+(#{ObjectMethod}+(,\s+#{ObjectMethod})*)/
16
+ Variable = /#{OpenTag}\s*(#{Helpers}|#{ObjectMethod})\s*#{CloseTag}/
17
+ CommaOutsideQuote = /,(?=(?:[^']|'[^']*')*$)/
18
+ end
19
+ end
@@ -0,0 +1,19 @@
1
+ class Cerubis
2
+ module Method
3
+ def self.included(base)
4
+ base.extend ClassMethods
5
+ end
6
+
7
+ def cerubis_respond_to?(meth)
8
+ availabe_methods = self.class.instance_variable_get(:@cerubis_method) || []
9
+ availabe_methods.include? meth
10
+ end
11
+
12
+ module ClassMethods
13
+ def cerubis_method(*args)
14
+ @cerubis_method ||= []
15
+ @cerubis_method += args
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,27 @@
1
+ class Cerubis
2
+ module Node
3
+ def self.included(base)
4
+ base.send(:attr, :content)
5
+ base.send(:attr, :parent)
6
+ base.send(:attr_accessor, :children)
7
+ end
8
+
9
+ def initialize(content, options)
10
+ @content = content
11
+ @parent = options[:parent]
12
+ @children = []
13
+ end
14
+
15
+ def context
16
+ parent.context
17
+ end
18
+
19
+ def children?
20
+ !@children.empty?
21
+ end
22
+
23
+ def pre_render
24
+ return content
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,4 @@
1
+ class Array
2
+ include Cerubis::Method
3
+ cerubis_method :empty?, :size, :first, :last
4
+ end
@@ -0,0 +1,4 @@
1
+ class Fixnum
2
+ include Cerubis::Method
3
+ cerubis_method :zero?
4
+ end
@@ -0,0 +1,4 @@
1
+ class Float
2
+ include Cerubis::Method
3
+ cerubis_method :zero?
4
+ end
@@ -0,0 +1,4 @@
1
+ class Hash
2
+ include Cerubis::Method
3
+ cerubis_method :size, :empty?
4
+ end
@@ -0,0 +1,4 @@
1
+ class String
2
+ include Cerubis::Method
3
+ cerubis_method :empty?, :size
4
+ end
@@ -0,0 +1,125 @@
1
+ require 'strscan'
2
+
3
+ class Cerubis
4
+ class Parser
5
+ attr :nodes
6
+
7
+ def initialize(content, options)
8
+ @content = content || ''
9
+ @options = options
10
+ @scanner = StringScanner.new(@content)
11
+ @nodes = []
12
+ parse!
13
+ end
14
+
15
+ private
16
+ def parse!
17
+ @default_regex = Regexp.new("(#{Matcher::OpenBlock})|(#{Matcher::CloseBlock})")
18
+ @current_regex = @default_regex
19
+ @blocks = []
20
+
21
+ record_positions until @scanner.eos?
22
+ raise SyntaxError, "An open '#{last_block_name}' block is not closed" if blocks_not_closed?
23
+
24
+ build_nodes
25
+ end
26
+
27
+ def build_nodes
28
+ if !@blocks.empty?
29
+ str_position = @blocks.first[1] # start of first block
30
+ @blocks.unshift([:text, 0, str_position.pred]) unless str_position.zero?
31
+
32
+ str_position = @blocks.last[2] # end of last block
33
+ if str_position != @content.size
34
+ @blocks << [:text, str_position.next, @content.size]
35
+ end
36
+
37
+ # loop and fill in missing ranges with text nodes
38
+ @blocks.each_with_index do |block, index|
39
+ next_block = @blocks[index.next]
40
+ position = block[2]
41
+
42
+ if next_block && next_block[1] != position.next
43
+ next_index = index.next
44
+ previous_position = next_block[1].pred
45
+
46
+ @blocks.insert(next_index, [:text, position.next, previous_position])
47
+ end
48
+ end
49
+ elsif !@content.empty? && @options[:parent].is_a?(Template)
50
+ @blocks << [:text, 0, @content.size]
51
+ end
52
+
53
+ create_node(@blocks.shift) until @blocks.empty?
54
+ end
55
+
56
+ def create_node(block)
57
+ start_of_str = block[1]
58
+ end_of_str = block[2]
59
+ content = @content[start_of_str..end_of_str]
60
+
61
+ if block[0] == :text
62
+ @nodes << TextNode.new(content, @options)
63
+ else
64
+ @nodes << BlockNode.new(content, @options)
65
+ end
66
+ end
67
+
68
+ def record_positions
69
+ @scanner.scan_until @current_regex
70
+ current_match = @scanner[0]
71
+
72
+ if current_match
73
+ if current_match =~ Matcher::OpenBlock
74
+ parse_open_block
75
+ elsif current_match =~ Matcher::CloseBlock
76
+ parse_close_block
77
+ end
78
+ else
79
+ @scanner.terminate
80
+ end
81
+ end
82
+
83
+ def nested_block?
84
+ return false if @blocks.empty?
85
+ return false if @blocks.last.is_a?(Array) && @blocks.last.size === 3
86
+ true
87
+ end
88
+
89
+ def blocks_not_closed?
90
+ # a proper closed block is prepresented as an array of 3 elements
91
+ # [block_name, start_of_block, end_of_block]
92
+ # any less than 3 and its not closed
93
+ !@blocks.empty? && (!@blocks.last.is_a?(Array) || @blocks.last.size < 3)
94
+ end
95
+
96
+ def parse_open_block
97
+ block_name = @scanner[2].to_sym
98
+
99
+ if nested_block?
100
+ @blocks << block_name
101
+ else
102
+ @blocks << [block_name, (@scanner.pos - @scanner.matched_size)]
103
+ end
104
+ end
105
+
106
+ def parse_close_block
107
+ raise SyntaxError, "Closing '#{@scanner[15]}' block was found without an opening" if @blocks.empty?
108
+ block_name = @scanner[15].to_sym
109
+
110
+ if @blocks.last == block_name # found the nested closing block
111
+ @blocks.pop
112
+
113
+ close_regex = Matcher::CloseBlockPlaceholder.sub('block_name', last_block_name.to_s)
114
+ @current_regex = Regexp.new("(#{Matcher::OpenBlock})|(#{close_regex})")
115
+ elsif @blocks && @blocks.last.is_a?(Array) && @blocks.last[0] == block_name
116
+ @blocks.last << @scanner.pos.pred
117
+ @current_regex = @default_regex
118
+ end
119
+ end
120
+
121
+ def last_block_name
122
+ @blocks.last.is_a?(Array) ? @blocks.last[0] : @blocks.last
123
+ end
124
+ end
125
+ end