cerubis 0.0.1

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.
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