handlebars 0.0.1 → 0.0.2

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