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.
Files changed (126) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +16 -0
  3. data/.gitmodules +3 -0
  4. data/.rspec +2 -0
  5. data/.travis.yml +25 -0
  6. data/Appraisals +26 -0
  7. data/CHANGELOG.md +2 -0
  8. data/Gemfile +8 -0
  9. data/LICENSE.txt +22 -0
  10. data/README.md +96 -0
  11. data/Rakefile +11 -0
  12. data/benchmark/rendering.rb +29 -0
  13. data/bin/fast_haml +4 -0
  14. data/ext/attribute_builder/attribute_builder.c +259 -0
  15. data/ext/attribute_builder/extconf.rb +3 -0
  16. data/fast_haml.gemspec +35 -0
  17. data/gemfiles/rails_4.0.gemfile +9 -0
  18. data/gemfiles/rails_4.1.gemfile +9 -0
  19. data/gemfiles/rails_4.2.gemfile +9 -0
  20. data/gemfiles/rails_edge.gemfile +10 -0
  21. data/haml_spec_test.rb +22 -0
  22. data/lib/fast_haml/ast.rb +112 -0
  23. data/lib/fast_haml/cli.rb +38 -0
  24. data/lib/fast_haml/compiler.rb +325 -0
  25. data/lib/fast_haml/element_parser.rb +288 -0
  26. data/lib/fast_haml/engine.rb +32 -0
  27. data/lib/fast_haml/filter_compilers/base.rb +29 -0
  28. data/lib/fast_haml/filter_compilers/cdata.rb +15 -0
  29. data/lib/fast_haml/filter_compilers/css.rb +15 -0
  30. data/lib/fast_haml/filter_compilers/escaped.rb +22 -0
  31. data/lib/fast_haml/filter_compilers/javascript.rb +15 -0
  32. data/lib/fast_haml/filter_compilers/plain.rb +17 -0
  33. data/lib/fast_haml/filter_compilers/preserve.rb +30 -0
  34. data/lib/fast_haml/filter_compilers/ruby.rb +13 -0
  35. data/lib/fast_haml/filter_compilers.rb +37 -0
  36. data/lib/fast_haml/filter_parser.rb +54 -0
  37. data/lib/fast_haml/html.rb +44 -0
  38. data/lib/fast_haml/indent_tracker.rb +84 -0
  39. data/lib/fast_haml/line_parser.rb +66 -0
  40. data/lib/fast_haml/parser.rb +251 -0
  41. data/lib/fast_haml/parser_utils.rb +17 -0
  42. data/lib/fast_haml/rails_handler.rb +10 -0
  43. data/lib/fast_haml/railtie.rb +8 -0
  44. data/lib/fast_haml/ruby_multiline.rb +23 -0
  45. data/lib/fast_haml/static_hash_parser.rb +113 -0
  46. data/lib/fast_haml/syntax_error.rb +10 -0
  47. data/lib/fast_haml/text_compiler.rb +69 -0
  48. data/lib/fast_haml/tilt.rb +16 -0
  49. data/lib/fast_haml/version.rb +3 -0
  50. data/lib/fast_haml.rb +10 -0
  51. data/spec/rails/Rakefile +6 -0
  52. data/spec/rails/app/assets/images/.keep +0 -0
  53. data/spec/rails/app/assets/javascripts/application.js +13 -0
  54. data/spec/rails/app/assets/stylesheets/application.css +15 -0
  55. data/spec/rails/app/controllers/application_controller.rb +5 -0
  56. data/spec/rails/app/controllers/books_controller.rb +8 -0
  57. data/spec/rails/app/controllers/concerns/.keep +0 -0
  58. data/spec/rails/app/helpers/application_helper.rb +2 -0
  59. data/spec/rails/app/mailers/.keep +0 -0
  60. data/spec/rails/app/models/.keep +0 -0
  61. data/spec/rails/app/models/book.rb +9 -0
  62. data/spec/rails/app/models/concerns/.keep +0 -0
  63. data/spec/rails/app/views/books/hello.html.haml +2 -0
  64. data/spec/rails/app/views/books/with_capture.html.haml +4 -0
  65. data/spec/rails/app/views/books/with_variables.html.haml +4 -0
  66. data/spec/rails/app/views/layouts/application.html.haml +9 -0
  67. data/spec/rails/bin/bundle +3 -0
  68. data/spec/rails/bin/rails +4 -0
  69. data/spec/rails/bin/rake +4 -0
  70. data/spec/rails/bin/setup +29 -0
  71. data/spec/rails/config/application.rb +12 -0
  72. data/spec/rails/config/boot.rb +3 -0
  73. data/spec/rails/config/database.yml +25 -0
  74. data/spec/rails/config/environment.rb +5 -0
  75. data/spec/rails/config/environments/development.rb +41 -0
  76. data/spec/rails/config/environments/production.rb +79 -0
  77. data/spec/rails/config/environments/test.rb +42 -0
  78. data/spec/rails/config/initializers/assets.rb +11 -0
  79. data/spec/rails/config/initializers/backtrace_silencers.rb +7 -0
  80. data/spec/rails/config/initializers/cookies_serializer.rb +3 -0
  81. data/spec/rails/config/initializers/filter_parameter_logging.rb +4 -0
  82. data/spec/rails/config/initializers/inflections.rb +16 -0
  83. data/spec/rails/config/initializers/mime_types.rb +4 -0
  84. data/spec/rails/config/initializers/secret_key_base.rb +6 -0
  85. data/spec/rails/config/initializers/session_store.rb +3 -0
  86. data/spec/rails/config/initializers/wrap_parameters.rb +14 -0
  87. data/spec/rails/config/locales/en.yml +23 -0
  88. data/spec/rails/config/routes.rb +7 -0
  89. data/spec/rails/config/secrets.yml +22 -0
  90. data/spec/rails/config.ru +4 -0
  91. data/spec/rails/db/seeds.rb +7 -0
  92. data/spec/rails/lib/assets/.keep +0 -0
  93. data/spec/rails/lib/tasks/.keep +0 -0
  94. data/spec/rails/log/.keep +0 -0
  95. data/spec/rails/public/404.html +67 -0
  96. data/spec/rails/public/422.html +67 -0
  97. data/spec/rails/public/500.html +66 -0
  98. data/spec/rails/public/favicon.ico +0 -0
  99. data/spec/rails/public/robots.txt +5 -0
  100. data/spec/rails/spec/requests/fast_haml_spec.rb +41 -0
  101. data/spec/rails/vendor/assets/javascripts/.keep +0 -0
  102. data/spec/rails/vendor/assets/stylesheets/.keep +0 -0
  103. data/spec/rails_helper.rb +4 -0
  104. data/spec/render/attribute_spec.rb +209 -0
  105. data/spec/render/comment_spec.rb +61 -0
  106. data/spec/render/doctype_spec.rb +62 -0
  107. data/spec/render/element_spec.rb +165 -0
  108. data/spec/render/filters/cdata_spec.rb +12 -0
  109. data/spec/render/filters/css_spec.rb +45 -0
  110. data/spec/render/filters/escaped_spec.rb +14 -0
  111. data/spec/render/filters/javascript_spec.rb +44 -0
  112. data/spec/render/filters/plain_spec.rb +24 -0
  113. data/spec/render/filters/preserve_spec.rb +25 -0
  114. data/spec/render/filters/ruby_spec.rb +13 -0
  115. data/spec/render/filters_spec.rb +11 -0
  116. data/spec/render/haml_comment_spec.rb +24 -0
  117. data/spec/render/multiline_spec.rb +39 -0
  118. data/spec/render/plain_spec.rb +20 -0
  119. data/spec/render/preserve_spec.rb +8 -0
  120. data/spec/render/sanitize_spec.rb +36 -0
  121. data/spec/render/script_spec.rb +74 -0
  122. data/spec/render/silent_script_spec.rb +97 -0
  123. data/spec/render/unescape_spec.rb +40 -0
  124. data/spec/spec_helper.rb +49 -0
  125. data/spec/tilt_spec.rb +33 -0
  126. 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,10 @@
1
+ module FastHaml
2
+ class RailsHandler
3
+ def call(template)
4
+ Engine.new(
5
+ use_html_safe: true,
6
+ generator: Temple::Generators::RailsOutputBuffer,
7
+ ).call(template.source)
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,8 @@
1
+ module FastHaml
2
+ class Railtie < ::Rails::Railtie
3
+ initializer :fast_haml do |app|
4
+ require 'fast_haml/rails_handler'
5
+ ActionView::Template.register_template_handler(:haml, FastHaml::RailsHandler.new)
6
+ end
7
+ end
8
+ 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,10 @@
1
+ module FastHaml
2
+ class SyntaxError < StandardError
3
+ attr_reader :lineno
4
+
5
+ def initialize(message, lineno)
6
+ super("#{message} at line #{lineno}")
7
+ @lineno = lineno
8
+ end
9
+ end
10
+ 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
@@ -0,0 +1,3 @@
1
+ module FastHaml
2
+ VERSION = "0.1.0"
3
+ end
data/lib/fast_haml.rb ADDED
@@ -0,0 +1,10 @@
1
+ require 'fast_haml/engine'
2
+ require 'fast_haml/tilt'
3
+ require 'fast_haml/version'
4
+
5
+ begin
6
+ gem 'rails'
7
+ require 'rails'
8
+ require 'fast_haml/railtie'
9
+ rescue LoadError
10
+ end
@@ -0,0 +1,6 @@
1
+ # Add your own tasks in files placed in lib/tasks ending in .rake,
2
+ # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
3
+
4
+ require File.expand_path('../config/application', __FILE__)
5
+
6
+ Rails.application.load_tasks
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
+ */
@@ -0,0 +1,5 @@
1
+ class ApplicationController < ActionController::Base
2
+ # Prevent CSRF attacks by raising an exception.
3
+ # For APIs, you may want to use :null_session instead.
4
+ protect_from_forgery with: :exception
5
+ end
@@ -0,0 +1,8 @@
1
+ class BooksController < ApplicationController
2
+ def hello
3
+ end
4
+
5
+ def with_variables
6
+ @book = Book.new(params.permit(:title))
7
+ end
8
+ end
File without changes
@@ -0,0 +1,2 @@
1
+ module ApplicationHelper
2
+ end
File without changes
File without changes
@@ -0,0 +1,9 @@
1
+ class Book
2
+ include ActiveModel::Model
3
+
4
+ attr_accessor :title
5
+
6
+ def to_s
7
+ "<span>#{title}</span>".html_safe
8
+ end
9
+ end
File without changes
@@ -0,0 +1,2 @@
1
+ .container
2
+ %span Hello, World