cerubis 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +10 -0
- data/.travis.yml +14 -0
- data/Gemfile +15 -0
- data/README.md +111 -0
- data/Rakefile +15 -0
- data/TODO.md +31 -0
- data/cerubis.gemspec +22 -0
- data/examples/blocks.rb +20 -0
- data/examples/full/blocks/script_block.rb +8 -0
- data/examples/full/config.rb +14 -0
- data/examples/full/example.rb +25 -0
- data/examples/full/helpers/html_helpers.rb +13 -0
- data/examples/full/helpers/include_helper.rb +15 -0
- data/examples/full/models/page.rb +17 -0
- data/examples/full/models/site.rb +13 -0
- data/examples/full/templates/_footer.cerubis +1 -0
- data/examples/full/templates/_header.cerubis +1 -0
- data/examples/full/templates/main.cerubis +31 -0
- data/examples/helpers.rb +22 -0
- data/examples/html.rb +18 -0
- data/examples/variables.rb +10 -0
- data/lib/cerubis.rb +55 -0
- data/lib/cerubis/block.rb +27 -0
- data/lib/cerubis/block_node.rb +37 -0
- data/lib/cerubis/blocks/if.rb +8 -0
- data/lib/cerubis/blocks/loop.rb +20 -0
- data/lib/cerubis/blocks/unless.rb +9 -0
- data/lib/cerubis/condition.rb +59 -0
- data/lib/cerubis/context.rb +30 -0
- data/lib/cerubis/helper.rb +12 -0
- data/lib/cerubis/matcher.rb +19 -0
- data/lib/cerubis/method.rb +19 -0
- data/lib/cerubis/node.rb +27 -0
- data/lib/cerubis/objects/array.rb +4 -0
- data/lib/cerubis/objects/fixnum.rb +4 -0
- data/lib/cerubis/objects/float.rb +4 -0
- data/lib/cerubis/objects/hash.rb +4 -0
- data/lib/cerubis/objects/string.rb +4 -0
- data/lib/cerubis/parser.rb +125 -0
- data/lib/cerubis/syntax_error.rb +4 -0
- data/lib/cerubis/template.rb +21 -0
- data/lib/cerubis/text_node.rb +10 -0
- data/lib/cerubis/variable_replacement.rb +34 -0
- data/lib/cerubis/version.rb +3 -0
- data/test/all.rb +3 -0
- data/test/cerubis/block_node_test.rb +36 -0
- data/test/cerubis/blocks/if_test.rb +24 -0
- data/test/cerubis/blocks/loop_test.rb +25 -0
- data/test/cerubis/blocks/unless_test.rb +25 -0
- data/test/cerubis/condition_test.rb +142 -0
- data/test/cerubis/context_test.rb +33 -0
- data/test/cerubis/helper_test.rb +17 -0
- data/test/cerubis/matcher_test.rb +20 -0
- data/test/cerubis/method_test.rb +60 -0
- data/test/cerubis/parser_test.rb +48 -0
- data/test/cerubis/template_test.rb +38 -0
- data/test/cerubis/text_node_test.rb +16 -0
- data/test/cerubis_test.rb +31 -0
- data/test/matchers/test_block_name.rb +25 -0
- data/test/matchers/test_close_block.rb +25 -0
- data/test/matchers/test_conditions.rb +21 -0
- data/test/matchers/test_helpers.rb +21 -0
- data/test/matchers/test_object_method.rb +37 -0
- data/test/matchers/test_open_block.rb +57 -0
- data/test/matchers/test_operators.rb +29 -0
- data/test/matchers/test_variable.rb +37 -0
- data/test/methods/test_array_methods.rb +21 -0
- data/test/methods/test_fixnum_methods.rb +6 -0
- data/test/methods/test_float_methods.rb +6 -0
- data/test/methods/test_hash_methods.rb +11 -0
- data/test/methods/test_string_methods.rb +11 -0
- data/test/nodes/test_node_defaults.rb +30 -0
- data/test/rendered_test.rb +159 -0
- data/test/test_helper.rb +34 -0
- metadata +149 -0
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,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,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,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
|
data/lib/cerubis/node.rb
ADDED
@@ -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,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
|