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