fast_haml 0.1.0
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.
- checksums.yaml +7 -0
- data/.gitignore +16 -0
- data/.gitmodules +3 -0
- data/.rspec +2 -0
- data/.travis.yml +25 -0
- data/Appraisals +26 -0
- data/CHANGELOG.md +2 -0
- data/Gemfile +8 -0
- data/LICENSE.txt +22 -0
- data/README.md +96 -0
- data/Rakefile +11 -0
- data/benchmark/rendering.rb +29 -0
- data/bin/fast_haml +4 -0
- data/ext/attribute_builder/attribute_builder.c +259 -0
- data/ext/attribute_builder/extconf.rb +3 -0
- data/fast_haml.gemspec +35 -0
- data/gemfiles/rails_4.0.gemfile +9 -0
- data/gemfiles/rails_4.1.gemfile +9 -0
- data/gemfiles/rails_4.2.gemfile +9 -0
- data/gemfiles/rails_edge.gemfile +10 -0
- data/haml_spec_test.rb +22 -0
- data/lib/fast_haml/ast.rb +112 -0
- data/lib/fast_haml/cli.rb +38 -0
- data/lib/fast_haml/compiler.rb +325 -0
- data/lib/fast_haml/element_parser.rb +288 -0
- data/lib/fast_haml/engine.rb +32 -0
- data/lib/fast_haml/filter_compilers/base.rb +29 -0
- data/lib/fast_haml/filter_compilers/cdata.rb +15 -0
- data/lib/fast_haml/filter_compilers/css.rb +15 -0
- data/lib/fast_haml/filter_compilers/escaped.rb +22 -0
- data/lib/fast_haml/filter_compilers/javascript.rb +15 -0
- data/lib/fast_haml/filter_compilers/plain.rb +17 -0
- data/lib/fast_haml/filter_compilers/preserve.rb +30 -0
- data/lib/fast_haml/filter_compilers/ruby.rb +13 -0
- data/lib/fast_haml/filter_compilers.rb +37 -0
- data/lib/fast_haml/filter_parser.rb +54 -0
- data/lib/fast_haml/html.rb +44 -0
- data/lib/fast_haml/indent_tracker.rb +84 -0
- data/lib/fast_haml/line_parser.rb +66 -0
- data/lib/fast_haml/parser.rb +251 -0
- data/lib/fast_haml/parser_utils.rb +17 -0
- data/lib/fast_haml/rails_handler.rb +10 -0
- data/lib/fast_haml/railtie.rb +8 -0
- data/lib/fast_haml/ruby_multiline.rb +23 -0
- data/lib/fast_haml/static_hash_parser.rb +113 -0
- data/lib/fast_haml/syntax_error.rb +10 -0
- data/lib/fast_haml/text_compiler.rb +69 -0
- data/lib/fast_haml/tilt.rb +16 -0
- data/lib/fast_haml/version.rb +3 -0
- data/lib/fast_haml.rb +10 -0
- data/spec/rails/Rakefile +6 -0
- data/spec/rails/app/assets/images/.keep +0 -0
- data/spec/rails/app/assets/javascripts/application.js +13 -0
- data/spec/rails/app/assets/stylesheets/application.css +15 -0
- data/spec/rails/app/controllers/application_controller.rb +5 -0
- data/spec/rails/app/controllers/books_controller.rb +8 -0
- data/spec/rails/app/controllers/concerns/.keep +0 -0
- data/spec/rails/app/helpers/application_helper.rb +2 -0
- data/spec/rails/app/mailers/.keep +0 -0
- data/spec/rails/app/models/.keep +0 -0
- data/spec/rails/app/models/book.rb +9 -0
- data/spec/rails/app/models/concerns/.keep +0 -0
- data/spec/rails/app/views/books/hello.html.haml +2 -0
- data/spec/rails/app/views/books/with_capture.html.haml +4 -0
- data/spec/rails/app/views/books/with_variables.html.haml +4 -0
- data/spec/rails/app/views/layouts/application.html.haml +9 -0
- data/spec/rails/bin/bundle +3 -0
- data/spec/rails/bin/rails +4 -0
- data/spec/rails/bin/rake +4 -0
- data/spec/rails/bin/setup +29 -0
- data/spec/rails/config/application.rb +12 -0
- data/spec/rails/config/boot.rb +3 -0
- data/spec/rails/config/database.yml +25 -0
- data/spec/rails/config/environment.rb +5 -0
- data/spec/rails/config/environments/development.rb +41 -0
- data/spec/rails/config/environments/production.rb +79 -0
- data/spec/rails/config/environments/test.rb +42 -0
- data/spec/rails/config/initializers/assets.rb +11 -0
- data/spec/rails/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/rails/config/initializers/cookies_serializer.rb +3 -0
- data/spec/rails/config/initializers/filter_parameter_logging.rb +4 -0
- data/spec/rails/config/initializers/inflections.rb +16 -0
- data/spec/rails/config/initializers/mime_types.rb +4 -0
- data/spec/rails/config/initializers/secret_key_base.rb +6 -0
- data/spec/rails/config/initializers/session_store.rb +3 -0
- data/spec/rails/config/initializers/wrap_parameters.rb +14 -0
- data/spec/rails/config/locales/en.yml +23 -0
- data/spec/rails/config/routes.rb +7 -0
- data/spec/rails/config/secrets.yml +22 -0
- data/spec/rails/config.ru +4 -0
- data/spec/rails/db/seeds.rb +7 -0
- data/spec/rails/lib/assets/.keep +0 -0
- data/spec/rails/lib/tasks/.keep +0 -0
- data/spec/rails/log/.keep +0 -0
- data/spec/rails/public/404.html +67 -0
- data/spec/rails/public/422.html +67 -0
- data/spec/rails/public/500.html +66 -0
- data/spec/rails/public/favicon.ico +0 -0
- data/spec/rails/public/robots.txt +5 -0
- data/spec/rails/spec/requests/fast_haml_spec.rb +41 -0
- data/spec/rails/vendor/assets/javascripts/.keep +0 -0
- data/spec/rails/vendor/assets/stylesheets/.keep +0 -0
- data/spec/rails_helper.rb +4 -0
- data/spec/render/attribute_spec.rb +209 -0
- data/spec/render/comment_spec.rb +61 -0
- data/spec/render/doctype_spec.rb +62 -0
- data/spec/render/element_spec.rb +165 -0
- data/spec/render/filters/cdata_spec.rb +12 -0
- data/spec/render/filters/css_spec.rb +45 -0
- data/spec/render/filters/escaped_spec.rb +14 -0
- data/spec/render/filters/javascript_spec.rb +44 -0
- data/spec/render/filters/plain_spec.rb +24 -0
- data/spec/render/filters/preserve_spec.rb +25 -0
- data/spec/render/filters/ruby_spec.rb +13 -0
- data/spec/render/filters_spec.rb +11 -0
- data/spec/render/haml_comment_spec.rb +24 -0
- data/spec/render/multiline_spec.rb +39 -0
- data/spec/render/plain_spec.rb +20 -0
- data/spec/render/preserve_spec.rb +8 -0
- data/spec/render/sanitize_spec.rb +36 -0
- data/spec/render/script_spec.rb +74 -0
- data/spec/render/silent_script_spec.rb +97 -0
- data/spec/render/unescape_spec.rb +40 -0
- data/spec/spec_helper.rb +49 -0
- data/spec/tilt_spec.rb +33 -0
- metadata +427 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
module FastHaml
|
|
2
|
+
class LineParser
|
|
3
|
+
attr_reader :lineno
|
|
4
|
+
|
|
5
|
+
def initialize(template_str)
|
|
6
|
+
@lines = template_str.each_line.map { |line| line.chomp.rstrip }
|
|
7
|
+
@lineno = 0
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def next_line
|
|
11
|
+
line = move_next
|
|
12
|
+
if is_multiline?(line)
|
|
13
|
+
next_multiline(line)
|
|
14
|
+
else
|
|
15
|
+
line
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def has_next?
|
|
20
|
+
@lineno < @lines.size
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
MULTILINE_SUFFIX = ' |'
|
|
26
|
+
|
|
27
|
+
# Regex to check for blocks with spaces around arguments. Not to be confused
|
|
28
|
+
# with multiline script.
|
|
29
|
+
# For example:
|
|
30
|
+
# foo.each do | bar |
|
|
31
|
+
# = bar
|
|
32
|
+
#
|
|
33
|
+
BLOCK_WITH_SPACES = /do\s*\|\s*[^\|]*\s+\|\z/o
|
|
34
|
+
|
|
35
|
+
def is_multiline?(line)
|
|
36
|
+
line = line.lstrip
|
|
37
|
+
line.end_with?(MULTILINE_SUFFIX) && line !~ BLOCK_WITH_SPACES
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def move_next
|
|
41
|
+
@lines[@lineno].tap do
|
|
42
|
+
@lineno += 1
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def move_back
|
|
47
|
+
@lineno -= 1
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def next_multiline(line)
|
|
51
|
+
buf = [line[0, line.size-1]]
|
|
52
|
+
while @lineno < @lines.size
|
|
53
|
+
line = move_next
|
|
54
|
+
|
|
55
|
+
if is_multiline?(line)
|
|
56
|
+
line = line[0, line.size-1]
|
|
57
|
+
buf << line.lstrip
|
|
58
|
+
else
|
|
59
|
+
move_back
|
|
60
|
+
break
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
buf.join
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
require 'fast_haml/ast'
|
|
2
|
+
require 'fast_haml/element_parser'
|
|
3
|
+
require 'fast_haml/filter_parser'
|
|
4
|
+
require 'fast_haml/indent_tracker'
|
|
5
|
+
require 'fast_haml/line_parser'
|
|
6
|
+
require 'fast_haml/parser_utils'
|
|
7
|
+
require 'fast_haml/ruby_multiline'
|
|
8
|
+
require 'fast_haml/syntax_error'
|
|
9
|
+
|
|
10
|
+
module FastHaml
|
|
11
|
+
class Parser
|
|
12
|
+
def initialize(options = {})
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def call(template_str)
|
|
16
|
+
@ast = Ast::Root.new
|
|
17
|
+
@stack = []
|
|
18
|
+
@line_parser = LineParser.new(template_str)
|
|
19
|
+
@indent_tracker = IndentTracker.new(on_enter: method(:indent_enter), on_leave: method(:indent_leave))
|
|
20
|
+
@filter_parser = FilterParser.new(@indent_tracker)
|
|
21
|
+
|
|
22
|
+
while @line_parser.has_next?
|
|
23
|
+
line = @line_parser.next_line
|
|
24
|
+
if !@ast.is_a?(Ast::HamlComment) && @filter_parser.enabled?
|
|
25
|
+
ast = @filter_parser.append(line)
|
|
26
|
+
if ast
|
|
27
|
+
@ast << ast
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
unless @filter_parser.enabled?
|
|
31
|
+
parse_line(line)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
ast = @filter_parser.finish
|
|
36
|
+
if ast
|
|
37
|
+
@ast << ast
|
|
38
|
+
end
|
|
39
|
+
@indent_tracker.finish
|
|
40
|
+
@ast
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
DOCTYPE_PREFIX = '!'
|
|
47
|
+
ELEMENT_PREFIX = '%'
|
|
48
|
+
SCRIPT_PREFIX = '='
|
|
49
|
+
COMMENT_PREFIX = '/'
|
|
50
|
+
SILENT_SCRIPT_PREFIX = '-'
|
|
51
|
+
DIV_ID_PREFIX = '#'
|
|
52
|
+
DIV_CLASS_PREFIX = '.'
|
|
53
|
+
FILTER_PREFIX = ':'
|
|
54
|
+
ESCAPE_PREFIX = '\\'
|
|
55
|
+
PRESERVE_PREFIX = '~'
|
|
56
|
+
SANITIZE_PREFIX = '&'
|
|
57
|
+
|
|
58
|
+
def parse_line(line)
|
|
59
|
+
text, indent = @indent_tracker.process(line, @line_parser.lineno)
|
|
60
|
+
|
|
61
|
+
if text.empty?
|
|
62
|
+
return
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
if @ast.is_a?(Ast::HamlComment)
|
|
66
|
+
@ast << Ast::Text.new(text)
|
|
67
|
+
return
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
case text[0]
|
|
71
|
+
when ESCAPE_PREFIX
|
|
72
|
+
parse_plain(text[1 .. -1])
|
|
73
|
+
when ELEMENT_PREFIX
|
|
74
|
+
parse_element(text)
|
|
75
|
+
when DOCTYPE_PREFIX
|
|
76
|
+
case
|
|
77
|
+
when text.start_with?('!!!')
|
|
78
|
+
parse_doctype(text)
|
|
79
|
+
when text.start_with?('!==')
|
|
80
|
+
parse_plain(text[3 .. -1].lstrip, escape_html: false)
|
|
81
|
+
when text[1] == SCRIPT_PREFIX
|
|
82
|
+
parse_script(text)
|
|
83
|
+
when text[1] == PRESERVE_PREFIX
|
|
84
|
+
parse_script("!=#{text[2 .. -1].lstrip}", preserve: true)
|
|
85
|
+
when text[1] == ' '
|
|
86
|
+
parse_plain(text[1 .. -1].lstrip, escape_html: false)
|
|
87
|
+
else
|
|
88
|
+
parse_plain(text)
|
|
89
|
+
end
|
|
90
|
+
when COMMENT_PREFIX
|
|
91
|
+
parse_comment(text)
|
|
92
|
+
when SCRIPT_PREFIX
|
|
93
|
+
if text[1] == SCRIPT_PREFIX
|
|
94
|
+
parse_plain(text[2 .. -1].strip)
|
|
95
|
+
else
|
|
96
|
+
parse_script(text)
|
|
97
|
+
end
|
|
98
|
+
when SILENT_SCRIPT_PREFIX
|
|
99
|
+
parse_silent_script(text)
|
|
100
|
+
when PRESERVE_PREFIX
|
|
101
|
+
# preserve has no meaning in non-html_escape mode
|
|
102
|
+
parse_script(text)
|
|
103
|
+
when DIV_ID_PREFIX, DIV_CLASS_PREFIX
|
|
104
|
+
if text.start_with?('#{')
|
|
105
|
+
parse_plain(text)
|
|
106
|
+
else
|
|
107
|
+
parse_line("#{indent}%div#{text}")
|
|
108
|
+
end
|
|
109
|
+
when FILTER_PREFIX
|
|
110
|
+
parse_filter(text)
|
|
111
|
+
when SANITIZE_PREFIX
|
|
112
|
+
case
|
|
113
|
+
when text.start_with?('&==')
|
|
114
|
+
parse_plain(text[3 .. -1].lstrip)
|
|
115
|
+
when text[1] == SCRIPT_PREFIX
|
|
116
|
+
parse_script(text)
|
|
117
|
+
when text[1] == PRESERVE_PREFIX
|
|
118
|
+
parse_script("&=#{text[2 .. -1].lstrip}", preserve: true)
|
|
119
|
+
else
|
|
120
|
+
parse_plain(text[1 .. -1].strip)
|
|
121
|
+
end
|
|
122
|
+
else
|
|
123
|
+
parse_plain(text)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def parse_doctype(text)
|
|
128
|
+
@ast << Ast::Doctype.new(text[3 .. -1].strip)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def parse_comment(text)
|
|
132
|
+
text = text[1, text.size-1].strip
|
|
133
|
+
comment = Ast::HtmlComment.new
|
|
134
|
+
comment.comment = text
|
|
135
|
+
if text[0] == '['
|
|
136
|
+
comment.conditional, rest = parse_conditional_comment(text)
|
|
137
|
+
text.replace(rest)
|
|
138
|
+
end
|
|
139
|
+
@ast << comment
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
CONDITIONAL_COMMENT_REGEX = /[\[\]]/o
|
|
143
|
+
|
|
144
|
+
def parse_conditional_comment(text)
|
|
145
|
+
s = StringScanner.new(text[1 .. -1])
|
|
146
|
+
depth = ParserUtils.balance(s, '[', ']')
|
|
147
|
+
if depth == 0
|
|
148
|
+
[s.pre_match, s.rest.lstrip]
|
|
149
|
+
else
|
|
150
|
+
syntax_error!('Unmatched brackets in conditional comment')
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def parse_plain(text, escape_html: true)
|
|
155
|
+
@ast << Ast::Text.new(text, escape_html)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def parse_element(text)
|
|
159
|
+
@ast << ElementParser.new(text, @line_parser.lineno, @line_parser).parse
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def parse_script(text, preserve: false)
|
|
163
|
+
m = text.match(/\A([!&~])?[=~] *(.*)\z/)
|
|
164
|
+
script = m[2]
|
|
165
|
+
if script.empty?
|
|
166
|
+
syntax_error!("No Ruby code to evaluate")
|
|
167
|
+
end
|
|
168
|
+
script += RubyMultiline.read(@line_parser, script)
|
|
169
|
+
node = Ast::Script.new([], script)
|
|
170
|
+
case m[1]
|
|
171
|
+
when '!'
|
|
172
|
+
node.escape_html = false
|
|
173
|
+
when '&'
|
|
174
|
+
node.escape_html = true
|
|
175
|
+
end
|
|
176
|
+
node.preserve = preserve
|
|
177
|
+
@ast << node
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def parse_silent_script(text)
|
|
181
|
+
if text.start_with?('-#')
|
|
182
|
+
@ast << Ast::HamlComment.new
|
|
183
|
+
return
|
|
184
|
+
end
|
|
185
|
+
script = text[/\A- *(.*)\z/, 1]
|
|
186
|
+
if script.empty?
|
|
187
|
+
syntax_error!("No Ruby code to evaluate")
|
|
188
|
+
end
|
|
189
|
+
script += RubyMultiline.read(@line_parser, script)
|
|
190
|
+
@ast << Ast::SilentScript.new([], script)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def parse_filter(text)
|
|
194
|
+
filter_name = text[/\A#{FILTER_PREFIX}(\w+)\z/, 1]
|
|
195
|
+
unless filter_name
|
|
196
|
+
syntax_error!("Invalid filter name: #{text}")
|
|
197
|
+
end
|
|
198
|
+
@filter_parser.start(filter_name)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def indent_enter(_, text)
|
|
202
|
+
@stack.push(@ast)
|
|
203
|
+
@ast = @ast.children.last
|
|
204
|
+
if @ast.is_a?(Ast::Element) && @ast.self_closing
|
|
205
|
+
syntax_error!('Illegal nesting: nesting within a self-closing tag is illegal')
|
|
206
|
+
end
|
|
207
|
+
if @ast.is_a?(Ast::HtmlComment) && !@ast.comment.empty?
|
|
208
|
+
syntax_error!('Illegal nesting: nesting within a html comment that already has content is illegal.')
|
|
209
|
+
end
|
|
210
|
+
if @ast.is_a?(Ast::HamlComment)
|
|
211
|
+
@indent_tracker.enter_comment!
|
|
212
|
+
end
|
|
213
|
+
nil
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def indent_leave(indent_level, text)
|
|
217
|
+
parent_ast = @stack.pop
|
|
218
|
+
case @ast
|
|
219
|
+
when Ast::Script, Ast::SilentScript
|
|
220
|
+
if indent_level == @indent_tracker.current_level
|
|
221
|
+
@ast.mid_block_keyword = mid_block_keyword?(text)
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
@ast = parent_ast
|
|
225
|
+
nil
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
MID_BLOCK_KEYWORDS = %w[else elsif rescue ensure end when]
|
|
229
|
+
START_BLOCK_KEYWORDS = %w[if begin case unless]
|
|
230
|
+
# Try to parse assignments to block starters as best as possible
|
|
231
|
+
START_BLOCK_KEYWORD_REGEX = /(?:\w+(?:,\s*\w+)*\s*=\s*)?(#{Regexp.union(START_BLOCK_KEYWORDS)})/
|
|
232
|
+
BLOCK_KEYWORD_REGEX = /^-?\s*(?:(#{Regexp.union(MID_BLOCK_KEYWORDS)})|#{START_BLOCK_KEYWORD_REGEX.source})\b/
|
|
233
|
+
|
|
234
|
+
def block_keyword(text)
|
|
235
|
+
m = text.match(BLOCK_KEYWORD_REGEX)
|
|
236
|
+
if m
|
|
237
|
+
m[1] || m[2]
|
|
238
|
+
else
|
|
239
|
+
nil
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def mid_block_keyword?(text)
|
|
244
|
+
MID_BLOCK_KEYWORDS.include?(block_keyword(text))
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def syntax_error!(message)
|
|
248
|
+
raise SyntaxError.new(message, @line_parser.lineno)
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module FastHaml
|
|
2
|
+
module ParserUtils
|
|
3
|
+
module_function
|
|
4
|
+
|
|
5
|
+
def balance(scanner, start, finish, depth = 1)
|
|
6
|
+
re = /(#{Regexp.escape(start)}|#{Regexp.escape(finish)})/
|
|
7
|
+
while depth > 0 && scanner.scan_until(re)
|
|
8
|
+
if scanner.matched == start
|
|
9
|
+
depth += 1
|
|
10
|
+
else
|
|
11
|
+
depth -= 1
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
depth
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module FastHaml
|
|
2
|
+
module RubyMultiline
|
|
3
|
+
def self.read(line_parser, current_text)
|
|
4
|
+
buf = []
|
|
5
|
+
while is_ruby_multiline?(current_text)
|
|
6
|
+
current_text = line_parser.next_line
|
|
7
|
+
buf << current_text
|
|
8
|
+
end
|
|
9
|
+
buf.join(' ')
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# `text' is a Ruby multiline block if it:
|
|
13
|
+
# - ends with a comma
|
|
14
|
+
# - but not "?," which is a character literal
|
|
15
|
+
# (however, "x?," is a method call and not a literal)
|
|
16
|
+
# - and not "?\," which is a character literal
|
|
17
|
+
def self.is_ruby_multiline?(text)
|
|
18
|
+
text && text.length > 1 && text[-1] == ?, &&
|
|
19
|
+
!((text[-3, 2] =~ /\W\?/) || text[-3, 2] == "?\\")
|
|
20
|
+
end
|
|
21
|
+
private_class_method :is_ruby_multiline?
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
require 'parser/current'
|
|
2
|
+
|
|
3
|
+
module FastHaml
|
|
4
|
+
class StaticHashParser
|
|
5
|
+
FAILURE_TAG = :failure
|
|
6
|
+
|
|
7
|
+
SPECIAL_ATTRIBUTES = %w[id class data].freeze
|
|
8
|
+
|
|
9
|
+
attr_reader :static_attributes, :dynamic_attributes
|
|
10
|
+
|
|
11
|
+
def initialize
|
|
12
|
+
@static_attributes = {}
|
|
13
|
+
@dynamic_attributes = {}
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def parse(text)
|
|
17
|
+
parser = ::Parser::CurrentRuby.new
|
|
18
|
+
parser.diagnostics.consumer = nil
|
|
19
|
+
buffer = ::Parser::Source::Buffer.new('(fast_haml)')
|
|
20
|
+
buffer.source = text
|
|
21
|
+
walk(parser.parse(buffer))
|
|
22
|
+
rescue ::Parser::SyntaxError
|
|
23
|
+
false
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def walk(node)
|
|
27
|
+
catch(FAILURE_TAG) do
|
|
28
|
+
walk_hash(node)
|
|
29
|
+
return true
|
|
30
|
+
end
|
|
31
|
+
false
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def walk_hash(node)
|
|
37
|
+
if node.type != :hash
|
|
38
|
+
throw FAILURE_TAG
|
|
39
|
+
end
|
|
40
|
+
node.children.each do |pair|
|
|
41
|
+
walk_pair(pair)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def walk_pair(node)
|
|
46
|
+
if node.type != :pair
|
|
47
|
+
throw FAILURE_TAG
|
|
48
|
+
end
|
|
49
|
+
key = node.children[0]
|
|
50
|
+
val = node.children[1]
|
|
51
|
+
|
|
52
|
+
if key_static = try_static_key(key)
|
|
53
|
+
try_static_value(key_static, val)
|
|
54
|
+
else
|
|
55
|
+
throw FAILURE_TAG
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def try_static_key(node)
|
|
60
|
+
case node.type
|
|
61
|
+
when :sym
|
|
62
|
+
node.location.expression.source.gsub(/\A:/, '').to_sym
|
|
63
|
+
when :int, :float, :str
|
|
64
|
+
eval(node.location.expression.source)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def try_static_value(key_static, node)
|
|
69
|
+
case node.type
|
|
70
|
+
when :sym
|
|
71
|
+
@static_attributes[key_static] = node.location.expression.source.gsub(/\A:/, '').to_sym
|
|
72
|
+
when :true, :false, :nil, :int, :float, :str
|
|
73
|
+
@static_attributes[key_static] = eval(node.location.expression.source)
|
|
74
|
+
when :dstr
|
|
75
|
+
@dynamic_attributes[key_static] = node.location.expression.source
|
|
76
|
+
when :send
|
|
77
|
+
if SPECIAL_ATTRIBUTES.include?(key_static.to_s)
|
|
78
|
+
throw FAILURE_TAG
|
|
79
|
+
else
|
|
80
|
+
@dynamic_attributes[key_static] = node.location.expression.source
|
|
81
|
+
end
|
|
82
|
+
when :hash
|
|
83
|
+
try_static_hash_value(key_static, node)
|
|
84
|
+
# TODO: Add array case
|
|
85
|
+
else
|
|
86
|
+
throw FAILURE_TAG
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def try_static_hash_value(key_static, node)
|
|
91
|
+
parser = self.class.new
|
|
92
|
+
if parser.walk(node)
|
|
93
|
+
merge_attributes(key_static, parser)
|
|
94
|
+
else
|
|
95
|
+
# TODO: Is it really impossible to optimize?
|
|
96
|
+
throw FAILURE_TAG
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def merge_attributes(key_static, parser)
|
|
101
|
+
unless parser.static_attributes.empty?
|
|
102
|
+
@static_attributes[key_static] = parser.static_attributes
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
unless parser.dynamic_attributes.empty?
|
|
106
|
+
expr = parser.dynamic_attributes.map do |k, v|
|
|
107
|
+
"#{k.inspect} => #{v}"
|
|
108
|
+
end.join(', ')
|
|
109
|
+
@dynamic_attributes[key_static] = "{#{expr}}"
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
require 'strscan'
|
|
2
|
+
require 'fast_haml/parser_utils'
|
|
3
|
+
|
|
4
|
+
module FastHaml
|
|
5
|
+
class TextCompiler
|
|
6
|
+
class InvalidInterpolation < StandardError
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def initialize(escape_html: true)
|
|
10
|
+
@escape_html = escape_html
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def compile(text, escape_html: @escape_html)
|
|
14
|
+
if contains_interpolation?(text)
|
|
15
|
+
compile_interpolation(text, escape_html: escape_html)
|
|
16
|
+
else
|
|
17
|
+
[:static, text]
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
INTERPOLATION_BEGIN = /(\\*)(#[\{$@])/o
|
|
24
|
+
|
|
25
|
+
def contains_interpolation?(text)
|
|
26
|
+
INTERPOLATION_BEGIN === text
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def compile_interpolation(text, escape_html: @escape_html)
|
|
30
|
+
s = StringScanner.new(text)
|
|
31
|
+
temple = [:multi]
|
|
32
|
+
pos = s.pos
|
|
33
|
+
while s.scan_until(INTERPOLATION_BEGIN)
|
|
34
|
+
escapes = s[1].size
|
|
35
|
+
pre = s.string.byteslice(pos ... (s.pos - s.matched.size))
|
|
36
|
+
temple << [:static, pre] << [:static, "\\" * (escapes/2)]
|
|
37
|
+
if escapes % 2 == 0
|
|
38
|
+
# perform interpolation
|
|
39
|
+
if s[2] == '#{'
|
|
40
|
+
temple << [:escape, escape_html, [:dynamic, find_close_brace(s)]]
|
|
41
|
+
else
|
|
42
|
+
var = s[2][-1]
|
|
43
|
+
s.scan(/\w+/)
|
|
44
|
+
var << s.matched
|
|
45
|
+
temple << [:escape, escape_html, [:dynamic, var]]
|
|
46
|
+
end
|
|
47
|
+
else
|
|
48
|
+
# escaped
|
|
49
|
+
temple << [:static, s[2]]
|
|
50
|
+
end
|
|
51
|
+
pos = s.pos
|
|
52
|
+
end
|
|
53
|
+
temple << [:static, s.rest]
|
|
54
|
+
temple
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
INTERPOLATION_BRACE = /[\{\}]/o
|
|
58
|
+
|
|
59
|
+
def find_close_brace(scanner)
|
|
60
|
+
pos = scanner.pos
|
|
61
|
+
depth = ParserUtils.balance(scanner, '{', '}')
|
|
62
|
+
if depth != 0
|
|
63
|
+
raise InvalidInterpolation.new(scanner.string)
|
|
64
|
+
else
|
|
65
|
+
scanner.string.byteslice(pos ... (scanner.pos-1))
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
require 'tilt'
|
|
2
|
+
require 'fast_haml/engine'
|
|
3
|
+
|
|
4
|
+
module FastHaml
|
|
5
|
+
class Tilt < Tilt::Template
|
|
6
|
+
def prepare
|
|
7
|
+
@code = Engine.new(options).call(data)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def precompiled_template(locals = {})
|
|
11
|
+
@code
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
::Tilt.register(Tilt, 'haml')
|
|
16
|
+
end
|
data/lib/fast_haml.rb
ADDED
data/spec/rails/Rakefile
ADDED
|
File without changes
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// This is a manifest file that'll be compiled into application.js, which will include all the files
|
|
2
|
+
// listed below.
|
|
3
|
+
//
|
|
4
|
+
// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
|
|
5
|
+
// or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path.
|
|
6
|
+
//
|
|
7
|
+
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
|
|
8
|
+
// compiled file.
|
|
9
|
+
//
|
|
10
|
+
// Read Sprockets README (https://github.com/sstephenson/sprockets#sprockets-directives) for details
|
|
11
|
+
// about supported directives.
|
|
12
|
+
//
|
|
13
|
+
//= require_tree .
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This is a manifest file that'll be compiled into application.css, which will include all the files
|
|
3
|
+
* listed below.
|
|
4
|
+
*
|
|
5
|
+
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
|
|
6
|
+
* or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
|
|
7
|
+
*
|
|
8
|
+
* You're free to add application-wide styles to this file and they'll appear at the bottom of the
|
|
9
|
+
* compiled file so the styles you add here take precedence over styles defined in any styles
|
|
10
|
+
* defined in the other CSS/SCSS files in this directory. It is generally better to create a new
|
|
11
|
+
* file per style scope.
|
|
12
|
+
*
|
|
13
|
+
*= require_tree .
|
|
14
|
+
*= require_self
|
|
15
|
+
*/
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|