handlebars 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -1,3 +1,4 @@
1
1
  pkg/*
2
2
  *.gem
3
3
  .bundle
4
+ session.vim
data/README.md ADDED
@@ -0,0 +1,39 @@
1
+ Handlebars
2
+ ==========
3
+
4
+ Handlebars is a implementation of [handlebars.js][2], an extension of
5
+ mustache by Yehuda Katz, in Ruby.
6
+
7
+
8
+ Current status
9
+ --------------
10
+
11
+ So far only the parser has been implemented. It supports the whole
12
+ handlebars syntax.
13
+
14
+
15
+ Installation
16
+ ------------
17
+
18
+ ### [RubyGems](http://rubygems.org/)
19
+
20
+ $ gem install handlebars
21
+
22
+
23
+ Acknowledgements
24
+ ----------------
25
+
26
+ Thanks to all the [implementers][3] of the original mustache gem.
27
+ Handlebars is based on their codebase and hard work.
28
+
29
+
30
+ Meta
31
+ ----
32
+
33
+ * Code: `git clone http://github.com/MSch/handlebars-ruby`
34
+ * Bugs: <http://github.com/MSch/handlebars-ruby/issues>
35
+ * Gems: <http://rubygems.org/gems/handlebars>
36
+
37
+ [1]:http://github.com/wycats/handlebars.js
38
+ [2]:http://yehudakatz.com/2010/09/09/announcing-handlebars-js/
39
+ [3]:http://github.com/defunkt/mustache/raw/master/CONTRIBUTORS
data/handlebars.gemspec CHANGED
@@ -15,7 +15,7 @@ Gem::Specification.new do |s|
15
15
  s.rubyforge_project = "handlebars"
16
16
 
17
17
  s.add_development_dependency "bundler", ">= 1.0.0"
18
- s.add_development_dependency "rspec", ">= 2.0.0"
18
+ s.add_development_dependency "rspec", "~> 2.0.0"
19
19
 
20
20
  s.files = `git ls-files`.split("\n")
21
21
  s.executables = `git ls-files`.split("\n").map{|f| f =~ /^bin\/(.*)/ ? $1 : nil}.compact
data/lib/handlebars.rb CHANGED
@@ -1,30 +1,5 @@
1
- require 'handlebars/compiler'
1
+ require 'handlebars/generator'
2
+ require 'handlebars/parser'
2
3
 
3
4
  class Handlebars
4
- attr_accessor :compiled_partials
5
- def compile(input)
6
- end
7
-
8
- def compile_to_string(input)
9
- end
10
-
11
- def compile_function_body(input)
12
- end
13
-
14
- def escape_text(input)
15
- CGI.escapeHTML(input)
16
- end
17
-
18
- def escape_expression(input)
19
- end
20
-
21
-
22
- def compile_partials(input)
23
- end
24
-
25
- def parse_path(input)
26
- end
27
-
28
- def filter_output(input, escape=true)
29
- end
30
5
  end
@@ -0,0 +1,4 @@
1
+ class Handlebars
2
+ class Generator
3
+ end
4
+ end
@@ -0,0 +1,240 @@
1
+ require 'strscan'
2
+
3
+ class Handlebars
4
+ class Parser
5
+ # A SyntaxError is raised when the Parser comes across unclosed
6
+ # tags, sections, illegal content in tags, or anything of that
7
+ # sort.
8
+ class SyntaxError < StandardError
9
+ def initialize(message, position)
10
+ @message = message
11
+ @lineno, @column, @line = position
12
+ @stripped_line = @line.strip
13
+ @stripped_column = @column - (@line.size - @line.lstrip.size)
14
+ end
15
+
16
+ def to_s
17
+ <<-EOF
18
+ #{@message}
19
+ Line #{@lineno}
20
+ #{@stripped_line}
21
+ #{' ' * @stripped_column}^
22
+ EOF
23
+ end
24
+ end
25
+
26
+ # After these types of tags, all whitespace will be skipped.
27
+ SKIP_WHITESPACE = [ '#', '^', '/' ]
28
+
29
+ # The content allowed in a tag name.
30
+ ALLOWED_CONTENT = /(\w|[.?!\/-])*/
31
+
32
+ # These types of tags allow any content,
33
+ # the rest only allow ALLOWED_CONTENT.
34
+ ANY_CONTENT = [ '!', '=' ]
35
+
36
+ attr_reader :scanner, :result
37
+ attr_writer :otag, :ctag
38
+
39
+ # Accepts an options hash which does nothing but may be used in
40
+ # the future.
41
+ def initialize(options = {})
42
+ @options = {}
43
+ end
44
+
45
+ # The opening tag delimiter. This may be changed at runtime.
46
+ def otag
47
+ @otag ||= '{{'
48
+ end
49
+
50
+ # The closing tag delimiter. This too may be changed at runtime.
51
+ def ctag
52
+ @ctag ||= '}}'
53
+ end
54
+
55
+ # Given a string template, returns an array of tokens.
56
+ def compile(template)
57
+ if template.respond_to?(:encoding)
58
+ @encoding = template.encoding
59
+ template = template.dup.force_encoding("BINARY")
60
+ else
61
+ @encoding = nil
62
+ end
63
+
64
+ # Keeps information about opened sections.
65
+ @sections = []
66
+ @result = [:multi]
67
+ @scanner = StringScanner.new(template)
68
+
69
+ # Scan until the end of the template.
70
+ until @scanner.eos?
71
+ scan_tags || scan_text
72
+ end
73
+
74
+ if !@sections.empty?
75
+ # We have parsed the whole file, but there's still opened sections.
76
+ type, pos, result = @sections.pop
77
+ error "Unclosed section #{type.inspect}", pos
78
+ end
79
+
80
+ @result
81
+ end
82
+
83
+ # Find {{mustaches}} and add them to the @result array.
84
+ def scan_tags
85
+ # Scan until we hit an opening delimiter.
86
+ return unless @scanner.scan(regexp(otag))
87
+
88
+ # Since {{= rewrites ctag, we store the ctag which should be used
89
+ # when parsing this specific tag.
90
+ current_ctag = self.ctag
91
+ type = @scanner.scan(/#|\^|\/|=|!|<|>|&|\{/)
92
+ @scanner.skip(/\s*/)
93
+
94
+ # ANY_CONTENT tags allow any character inside of them, while
95
+ # other tags (such as variables) are more strict.
96
+ if ANY_CONTENT.include?(type)
97
+ r = /\s*#{regexp(type)}?#{regexp(current_ctag)}/
98
+ content = scan_until_exclusive(r)
99
+ else
100
+ content = @scanner.scan(ALLOWED_CONTENT)
101
+ end
102
+
103
+ # We found {{ but we can't figure out what's going on inside.
104
+ # This applies to all tags except handlebar's shortened {{^}}
105
+ if type != '^'
106
+ error "Illegal content in tag" if content.empty?
107
+ end
108
+
109
+ def find_context
110
+ # A space after the helper indicates a context 'switch'
111
+ if @scanner.peek(1) == ' '
112
+ @scanner.skip(/ /)
113
+ @scanner.scan(ALLOWED_CONTENT)
114
+ else
115
+ nil
116
+ end
117
+ end
118
+
119
+ # Based on the sigil, do what needs to be done.
120
+ case type
121
+ when '#'
122
+ block = [:multi]
123
+ @result << [:mustache, :section, content, find_context(), block]
124
+ @sections << [content, position, @result]
125
+ @result = block
126
+ when '^'
127
+ if content.empty?
128
+ # We are dealing with handlebar's shortened {{^}}
129
+
130
+ # Close the section
131
+ section, pos, result = @sections.pop
132
+ @result = result
133
+ if section.nil?
134
+ error "Inverting unopened section"
135
+ end
136
+
137
+ # Open a new inverted section with the same name
138
+ block = [:multi]
139
+ @result << [:mustache, :inverted_section, section, block]
140
+ @sections << [section, position, @result]
141
+ @result = block
142
+ else
143
+ block = [:multi]
144
+ @result << [:mustache, :inverted_section, content, block]
145
+ @sections << [content, position, @result]
146
+ @result = block
147
+ end
148
+ when '/'
149
+ section, pos, result = @sections.pop
150
+ @result = result
151
+
152
+ if section.nil?
153
+ error "Closing unopened #{content.inspect}"
154
+ elsif section != content
155
+ error "Unclosed section #{section.inspect}", pos
156
+ end
157
+ when '!'
158
+ # ignore comments
159
+ when '='
160
+ self.otag, self.ctag = content.split(' ', 2)
161
+ when '>', '<'
162
+ @result << [:mustache, :partial, content]
163
+ when '{', '&'
164
+ # The closing } in unescaped tags is just a hack for
165
+ # aesthetics.
166
+ type = "}" if type == "{"
167
+ @result << [:mustache, :utag, content]
168
+ else
169
+ @result << [:mustache, :etag, content, find_context()]
170
+ end
171
+
172
+ # Skip whitespace and any balancing sigils after the content
173
+ # inside this tag.
174
+ @scanner.skip(/\s+/)
175
+ @scanner.skip(regexp(type)) if type
176
+
177
+ # Try to find the closing tag.
178
+ unless close = @scanner.scan(regexp(current_ctag))
179
+ error "Unclosed tag"
180
+ end
181
+
182
+ # Skip whitespace following this tag if we need to.
183
+ @scanner.skip(/\s+/) if SKIP_WHITESPACE.include?(type)
184
+ end
185
+
186
+ # Try to find static text, e.g. raw HTML with no {{mustaches}}.
187
+ def scan_text
188
+ text = scan_until_exclusive(regexp(otag))
189
+
190
+ if text.nil?
191
+ # Couldn't find any otag, which means the rest is just static text.
192
+ text = @scanner.rest
193
+ # Mark as done.
194
+ @scanner.clear
195
+ end
196
+
197
+ text.force_encoding(@encoding) if @encoding
198
+
199
+ @result << [:static, text]
200
+ end
201
+
202
+ # Scans the string until the pattern is matched. Returns the substring
203
+ # *excluding* the end of the match, advancing the scan pointer to that
204
+ # location. If there is no match, nil is returned.
205
+ def scan_until_exclusive(regexp)
206
+ pos = @scanner.pos
207
+ if @scanner.scan_until(regexp)
208
+ @scanner.pos -= @scanner.matched.size
209
+ @scanner.pre_match[pos..-1]
210
+ end
211
+ end
212
+
213
+ # Returns [lineno, column, line]
214
+ def position
215
+ # The rest of the current line
216
+ rest = @scanner.check_until(/\n|\Z/).to_s.chomp
217
+
218
+ # What we have parsed so far
219
+ parsed = @scanner.string[0...@scanner.pos]
220
+
221
+ lines = parsed.split("\n")
222
+
223
+ [ lines.size, lines.last.size - 1, lines.last + rest ]
224
+ end
225
+
226
+ # Used to quickly convert a string into a regular expression
227
+ # usable by the string scanner.
228
+ def regexp(thing)
229
+ /#{Regexp.escape(thing)}/
230
+ end
231
+
232
+ # Raises a SyntaxError. The message should be the name of the
233
+ # error - other details such as line number and position are
234
+ # handled for you.
235
+ def error(message, pos = position)
236
+ raise SyntaxError.new(message, pos)
237
+ end
238
+ end
239
+ end
240
+
@@ -1,3 +1,3 @@
1
1
  module Handlebars
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.2"
3
3
  end
@@ -0,0 +1,5 @@
1
+ require 'spec_helper'
2
+ require 'handlebars'
3
+
4
+ describe Handlebars::Generator do
5
+ end
@@ -0,0 +1,163 @@
1
+ require 'spec_helper'
2
+ require 'handlebars'
3
+
4
+ describe Handlebars::Parser do
5
+
6
+ it 'fails when the handlebars inverted section syntax is used without an opened section' do
7
+ lexer = Handlebars::Parser.new
8
+ proc {
9
+ lexer.compile(<<-EOF)
10
+ {{^}}
11
+ <h1>No projects</h1>
12
+ {{/project}}
13
+ EOF
14
+ }.should raise_error(Handlebars::Parser::SyntaxError)
15
+ end
16
+
17
+ it 'parses the handlebars inverted section syntax' do
18
+ lexer = Handlebars::Parser.new
19
+ tokens = lexer.compile(<<-EOF)
20
+ {{#project}}
21
+ <h1>{{name}}</h1>
22
+ <div>{{body}}</div>
23
+ {{^}}
24
+ <h1>No projects</h1>
25
+ {{/project}}
26
+ EOF
27
+ expected = [:multi,
28
+ [:mustache, :section, "project", nil, [:multi,
29
+ [:static, "<h1>"],
30
+ [:mustache, :etag, "name", nil],
31
+ [:static, "</h1>\n<div>"],
32
+ [:mustache, :etag, "body", nil],
33
+ [:static, "</div>\n"]]],
34
+ [:mustache, :inverted_section, "project", [:multi,
35
+ [:static, "<h1>No projects</h1>\n"]]]]
36
+ tokens.should eq(expected)
37
+ end
38
+
39
+ it 'parses the mustache inverted section syntax' do
40
+ lexer = Handlebars::Parser.new
41
+ tokens = lexer.compile(<<-EOF)
42
+ {{#project}}
43
+ <h1>{{name}}</h1>
44
+ <div>{{body}}</div>
45
+ {{/project}}
46
+ {{^project}}
47
+ <h1>No projects</h1>
48
+ {{/project}}
49
+ EOF
50
+ expected = [:multi,
51
+ [:mustache, :section, "project", nil, [:multi,
52
+ [:static, "<h1>"],
53
+ [:mustache, :etag, "name", nil],
54
+ [:static, "</h1>\n<div>"],
55
+ [:mustache, :etag, "body", nil],
56
+ [:static, "</div>\n"]]],
57
+ [:mustache, :inverted_section, "project", [:multi,
58
+ [:static, "<h1>No projects</h1>\n"]]]]
59
+ tokens.should eq(expected)
60
+ end
61
+
62
+ it 'parses helpers with context paths' do
63
+ lexer = Handlebars::Parser.new
64
+ tokens = lexer.compile(<<-EOF)
65
+ <h1>{{helper context}}</h1>
66
+ <h1>{{helper ..}}</h1>
67
+ {{#items ..}}
68
+ haha
69
+ {{haha}}
70
+ {{/items}}
71
+ EOF
72
+
73
+ expected = [:multi,
74
+ [:static, "<h1>"],
75
+ [:mustache, :etag, "helper", "context"],
76
+ [:static, "</h1>\n<h1>"],
77
+ [:mustache, :etag, "helper", ".."],
78
+ [:static, "</h1>\n"],
79
+ [:mustache, :section, "items", "..", [:multi,
80
+ [:static, "haha\n"],
81
+ [:mustache, :etag, "haha", nil],
82
+ [:static, "\n"]]]]
83
+ tokens.should eq(expected)
84
+ end
85
+
86
+ it 'parses extended paths' do
87
+ lexer = Handlebars::Parser.new
88
+ tokens = lexer.compile(<<-EOF)
89
+ <h1>{{../../header}}</h1>
90
+ <div>{{./header}}
91
+ {{hans/hubert/header}}</div>
92
+ {{#ines/ingrid/items}}
93
+ a
94
+ {{/ines/ingrid/items}}
95
+ EOF
96
+
97
+ expected = [:multi,
98
+ [:static, "<h1>"],
99
+ [:mustache, :etag, "../../header", nil],
100
+ [:static, "</h1>\n<div>"],
101
+ [:mustache, :etag, "./header", nil],
102
+ [:static, "\n"],
103
+ [:mustache, :etag, "hans/hubert/header", nil],
104
+ [:static, "</div>\n"],
105
+ [:mustache, :section, "ines/ingrid/items", nil, [:multi,
106
+ [:static, "a\n"]]]]
107
+ tokens.should eq(expected)
108
+ end
109
+
110
+ it 'parses the mustache example' do
111
+ lexer = Handlebars::Parser.new
112
+ tokens = lexer.compile(<<-EOF)
113
+ <h1>{{header}}</h1>
114
+ {{#items}}
115
+ {{#first}}
116
+ <li><strong>{{name}}</strong></li>
117
+ {{/first}}
118
+ {{#link}}
119
+ <li><a href="{{url}}">{{name}}</a></li>
120
+ {{/link}}
121
+ {{/items}}
122
+
123
+ {{#empty}}
124
+ <p>The list is empty.</p>
125
+ {{/empty}}
126
+ EOF
127
+
128
+ expected = [:multi,
129
+ [:static, "<h1>"],
130
+ [:mustache, :etag, "header", nil],
131
+ [:static, "</h1>\n"],
132
+ [:mustache,
133
+ :section,
134
+ "items",
135
+ nil,
136
+ [:multi,
137
+ [:mustache,
138
+ :section,
139
+ "first",
140
+ nil,
141
+ [:multi,
142
+ [:static, "<li><strong>"],
143
+ [:mustache, :etag, "name", nil],
144
+ [:static, "</strong></li>\n"]]],
145
+ [:mustache,
146
+ :section,
147
+ "link",
148
+ nil,
149
+ [:multi,
150
+ [:static, "<li><a href=\""],
151
+ [:mustache, :etag, "url", nil],
152
+ [:static, "\">"],
153
+ [:mustache, :etag, "name", nil],
154
+ [:static, "</a></li>\n"]]]]],
155
+ [:mustache,
156
+ :section,
157
+ "empty",
158
+ nil,
159
+ [:multi, [:static, "<p>The list is empty.</p>\n"]]]]
160
+
161
+ tokens.should eq(expected)
162
+ end
163
+ end
metadata CHANGED
@@ -5,8 +5,8 @@ version: !ruby/object:Gem::Version
5
5
  segments:
6
6
  - 0
7
7
  - 0
8
- - 1
9
- version: 0.0.1
8
+ - 2
9
+ version: 0.0.2
10
10
  platform: ruby
11
11
  authors:
12
12
  - Martin Schuerrer
@@ -38,7 +38,7 @@ dependencies:
38
38
  requirement: &id002 !ruby/object:Gem::Requirement
39
39
  none: false
40
40
  requirements:
41
- - - ">="
41
+ - - ~>
42
42
  - !ruby/object:Gem::Version
43
43
  segments:
44
44
  - 2
@@ -59,13 +59,16 @@ extra_rdoc_files: []
59
59
  files:
60
60
  - .gitignore
61
61
  - Gemfile
62
+ - README.md
62
63
  - Rakefile
63
64
  - handlebars.gemspec
64
65
  - lib/handlebars.rb
65
- - lib/handlebars/compiler.rb
66
+ - lib/handlebars/generator.rb
67
+ - lib/handlebars/parser.rb
66
68
  - lib/handlebars/version.rb
69
+ - spec/generator_spec.rb
70
+ - spec/parser_spec.rb
67
71
  - spec/spec_helper.rb
68
- - spec/test_spec.rb
69
72
  has_rdoc: true
70
73
  homepage: http://github.com/MSch/handlebars-ruby
71
74
  licenses: []
@@ -1,96 +0,0 @@
1
- require 'stringio'
2
-
3
- class Handlebars
4
- class Compiler
5
- attr_accessor :input, :pointer, :mustache, :text, :fn, :newlines, :comment, :escaped, :partial, :inverted, :end_condition, :continue_inverted
6
- def initialize(input)
7
- @input = input
8
- @pointer = -1
9
- @mustache = false
10
- @text = ''
11
- @fn = 'out = ""; lookup = nil; '
12
- @newlines = ''
13
- @comment = false
14
- @escaped = true
15
- @partial = false
16
- @inverted = false
17
- @end_condition = nil
18
- @continue_inverted = false
19
- end
20
-
21
- def add_text
22
- if not @text.empty?
23
- @fn << 'out = out + \'' + CGI.escapeHTML(@text) + '\''
24
- @fn << @newlines
25
- @newlines = ''
26
- @text = ''
27
- end
28
- end
29
-
30
- def parse_mustache
31
- chr, part, mustache, param = nil
32
- @s = StringScanner.new(input)
33
-
34
- next_char = @s.peek
35
- case next_char
36
- when '!'
37
- @comment = true
38
- when '#'
39
- @open_block = true
40
- when '>'
41
- @partial = true
42
- when '^'
43
- @inverted = true
44
- @open_block = true
45
- when '{'
46
- @escaped = false
47
- when '&'
48
- @escaped = false
49
- end
50
- @s.getch
51
-
52
- add_text
53
- @mustache = ' '
54
-
55
- while(chr = @s.getch)
56
- if @mustache && chr == '}' && @s.peek == '}'
57
- parts = @mustache.chomp.split(/\s+/)
58
- mustache = parts[0]
59
- param = lookup_for(parts[1])
60
-
61
- @mustace = false
62
-
63
- # finish reading off the close of the handlebars
64
- @s.getch
65
-
66
- # {{{expression}} is techically valid, but if we started with {{{ we'll try to read
67
- # }}} off of the close of the handlebars
68
- @s.getch if (!@escaped && @s.peek == '}')
69
-
70
- if @comment
71
- @comment = false
72
- return
73
- elsif @partial
74
- add_partial(mustache, param)
75
- return
76
- elsif @inverted
77
- add_inverted_section(mustache)
78
- return
79
- elsif @open_block
80
- add_block(mustache, param, parts)
81
- return
82
- else
83
- return this.add_expression(mustache, param)
84
- end
85
-
86
- @escaped = true
87
- elsif @comment
88
- ;
89
- else
90
- @mustace << chr
91
- end
92
- end
93
- end
94
-
95
- end
96
- end
data/spec/test_spec.rb DELETED
@@ -1,8 +0,0 @@
1
- require 'spec_helper'
2
- require 'handlebars'
3
-
4
- describe 'something' do
5
- it 'instanziates Compiler' do
6
- Handlebars::Compiler.new('')
7
- end
8
- end