fast_haml 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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,288 @@
1
+ require 'strscan'
2
+ require 'fast_haml/ast'
3
+ require 'fast_haml/parser_utils'
4
+ require 'fast_haml/ruby_multiline'
5
+ require 'fast_haml/syntax_error'
6
+
7
+ module FastHaml
8
+ class ElementParser
9
+ def initialize(text, lineno, line_parser)
10
+ @text = text
11
+ @lineno = lineno
12
+ @line_parser = line_parser
13
+ end
14
+
15
+ ELEMENT_REGEXP = /\A%([-:\w]+)([-:\w.#]*)(.+)?\z/o
16
+
17
+ def parse
18
+ m = @text.match(ELEMENT_REGEXP)
19
+ unless m
20
+ syntax_error!('Invalid element declaration')
21
+ end
22
+
23
+ element = Ast::Element.new
24
+ element.tag_name = m[1]
25
+ element.static_class, element.static_id = parse_class_and_id(m[2])
26
+ rest = m[3] || ''
27
+
28
+ element.attributes, rest = parse_attributes(rest.lstrip)
29
+ element.nuke_inner_whitespace, element.nuke_outer_whitespace, rest = parse_nuke_whitespace(rest)
30
+ element.self_closing, rest = parse_self_closing(rest)
31
+ element.oneline_child = parse_oneline_child(rest)
32
+
33
+ element
34
+ end
35
+
36
+ private
37
+
38
+ def parse_class_and_id(class_and_id)
39
+ classes = []
40
+ id = ''
41
+ class_and_id.scan(/([#.])([-:_a-zA-Z0-9]+)/) do |type, prop|
42
+ case type
43
+ when '.'
44
+ classes << prop
45
+ when '#'
46
+ id = prop
47
+ end
48
+ end
49
+
50
+ [classes.join(' '), id]
51
+ end
52
+
53
+ OLD_ATTRIBUTE_BEGIN = '{'
54
+ NEW_ATTRIBUTE_BEGIN = '('
55
+
56
+ def parse_attributes(rest)
57
+ old_attributes = ''
58
+ new_attributes = ''
59
+
60
+ loop do
61
+ case rest[0]
62
+ when OLD_ATTRIBUTE_BEGIN
63
+ unless old_attributes.empty?
64
+ break
65
+ end
66
+ old_attributes, rest = parse_old_attributes(rest)
67
+ when NEW_ATTRIBUTE_BEGIN
68
+ unless new_attributes.empty?
69
+ break
70
+ end
71
+ new_attributes, rest = parse_new_attributes(rest)
72
+ else
73
+ break
74
+ end
75
+ end
76
+
77
+ attributes = old_attributes
78
+ unless new_attributes.empty?
79
+ t = to_old_syntax(new_attributes)
80
+ if attributes.empty?
81
+ attributes = t
82
+ else
83
+ attributes << ", " << t
84
+ end
85
+ end
86
+ [attributes, rest]
87
+ end
88
+
89
+ def parse_old_attributes(text)
90
+ text = text.dup
91
+ s = StringScanner.new(text)
92
+ s.pos = 1
93
+ depth = 1
94
+ loop do
95
+ depth = ParserUtils.balance(s, '{', '}')
96
+ if depth == 0
97
+ attr = s.pre_match + s.matched
98
+ return [attr[1, attr.size-2], s.rest.lstrip]
99
+ else
100
+ if text[-1] == ',' && @line_parser.has_next?
101
+ text << @line_parser.next_line
102
+ else
103
+ syntax_error!('Unmatched brace')
104
+ end
105
+ end
106
+ end
107
+ end
108
+
109
+ def parse_new_attributes(text)
110
+ text = text.dup
111
+ s = StringScanner.new(text)
112
+ s.pos = 1
113
+ depth = 1
114
+ new_attributes = []
115
+ loop do
116
+ pre_pos = s.pos
117
+ depth = ParserUtils.balance(s, '(', ')', depth)
118
+ if depth == 0
119
+ t = s.string.byteslice(pre_pos ... s.pos-1)
120
+ new_attributes.concat(parse_new_attribute_list(t))
121
+ return [new_attributes, s.rest.lstrip]
122
+ else
123
+ if @line_parser.has_next?
124
+ text << ' ' << @line_parser.next_line
125
+ else
126
+ syntax_error!('Unmatched paren')
127
+ end
128
+ end
129
+ end
130
+ end
131
+
132
+ def parse_new_attribute_list(text)
133
+ s = StringScanner.new(text)
134
+ list = []
135
+ until s.eos?
136
+ name = scan_key(s)
137
+ s.skip(/\s*/)
138
+
139
+ if scan_operator(s)
140
+ s.skip(/\s*/)
141
+ value = scan_value(s)
142
+ else
143
+ value = 'true'
144
+ end
145
+ s.skip(/\s*/)
146
+
147
+ list << [name, value]
148
+ end
149
+ list
150
+ end
151
+
152
+ def scan_key(scanner)
153
+ scanner.scan(/[-:\w]+/).tap do |name|
154
+ unless name
155
+ syntax_error!('Invalid attribute list (missing attributename)')
156
+ end
157
+ end
158
+ end
159
+
160
+ def scan_operator(scanner)
161
+ scanner.skip(/=/)
162
+ end
163
+
164
+ def scan_value(scanner)
165
+ if quote = scanner.scan(/["']/)
166
+ scan_quoted_value(scanner, quote)
167
+ else
168
+ scan_variable_value(scanner)
169
+ end
170
+ end
171
+
172
+ def scan_quoted_value(scanner, quote)
173
+ re = /((?:\\.|\#(?!\{)|[^#{quote}\\#])*)(#{quote}|#\{)/
174
+ pos = scanner.pos
175
+ loop do
176
+ unless scanner.scan(re)
177
+ syntax_error!('Invalid attribute list (mismatched quotation)')
178
+ end
179
+ if scanner[2] == quote
180
+ break
181
+ end
182
+ depth = ParserUtils.balance(scanner, '{', '}')
183
+ if depth != 0
184
+ syntax_error!('Invalid attribute list (mismatched interpolation)')
185
+ end
186
+ end
187
+ str = scanner.string.byteslice(pos-1 .. scanner.pos-1)
188
+
189
+ # Even if the quote is single, string interpolation is performed in Haml.
190
+ str[0] = '"'
191
+ str[-1] = '"'
192
+ str
193
+ end
194
+
195
+ def scan_variable_value(scanner)
196
+ scanner.scan(/(@@?|\$)?\w+/).tap do |var|
197
+ unless var
198
+ syntax_error!('Invalid attribute list (invalid variable name)')
199
+ end
200
+ end
201
+ end
202
+
203
+ def to_old_syntax(new_attributes)
204
+ new_attributes.map { |k, v| "#{k.inspect} => #{v}" }.join(', ')
205
+ end
206
+
207
+ def parse_nuke_whitespace(rest)
208
+ m = rest.match(/\A(><|<>|[><])(.*)\z/)
209
+ if m
210
+ nuke_whitespace = m[1]
211
+ [
212
+ nuke_whitespace.include?('<'),
213
+ nuke_whitespace.include?('>'),
214
+ m[2],
215
+ ]
216
+ else
217
+ [false, false, rest]
218
+ end
219
+ end
220
+
221
+ def parse_self_closing(rest)
222
+ if rest[0] == '/'
223
+ if rest.size > 1
224
+ syntax_error!("Self-closing tags can't have content")
225
+ end
226
+ [true, '']
227
+ else
228
+ [false, rest]
229
+ end
230
+ end
231
+
232
+ def parse_oneline_child(rest)
233
+ case rest[0]
234
+ when '=', '~'
235
+ if rest[1] == '='
236
+ Ast::Text.new(rest[2 .. -1].strip)
237
+ else
238
+ script = rest[1 .. -1].lstrip
239
+ if script.empty?
240
+ syntax_error!('No Ruby code to evaluate')
241
+ end
242
+ script += RubyMultiline.read(@line_parser, script)
243
+ Ast::Script.new([], script)
244
+ end
245
+ when '&'
246
+ case
247
+ when rest.start_with?('&==')
248
+ Ast::Text.new(rest[3 .. -1].lstrip)
249
+ when rest[1] == '=' || rest[1] == '~'
250
+ script = rest[2 .. -1].lstrip
251
+ if script.empty?
252
+ syntax_error!('No Ruby code to evaluate')
253
+ end
254
+ script += RubyMultiline.read(@line_parser, script)
255
+ Ast::Script.new([], script, true, rest[1] == '~')
256
+ else
257
+ Ast::Text.new(rest[1 .. -1].strip)
258
+ end
259
+ when '!'
260
+ case
261
+ when rest.start_with?('!==')
262
+ Ast::Text.new(rest[3 .. -1].lstrip, false)
263
+ when rest[1] == '=' || rest[1] == '~'
264
+ script = rest[2 .. -1].lstrip
265
+ if script.empty?
266
+ syntax_error!('No Ruby code to evaluate')
267
+ end
268
+ script += RubyMultiline.read(@line_parser, script)
269
+ Ast::Script.new([], script, false, rest[1] == '~')
270
+ Ast::Script.new([], script, false, true)
271
+ else
272
+ Ast::Text.new(rest[1 .. -1].lstrip, false)
273
+ end
274
+ else
275
+ rest = rest.lstrip
276
+ if rest.empty?
277
+ nil
278
+ else
279
+ Ast::Text.new(rest)
280
+ end
281
+ end
282
+ end
283
+
284
+ def syntax_error!(message)
285
+ raise SyntaxError.new(message, @lineno)
286
+ end
287
+ end
288
+ end
@@ -0,0 +1,32 @@
1
+ require 'temple'
2
+ require 'fast_haml/compiler'
3
+ require 'fast_haml/html'
4
+ require 'fast_haml/parser'
5
+
6
+ module FastHaml
7
+ class Engine < Temple::Engine
8
+ define_options(
9
+ generator: Temple::Generators::ArrayBuffer,
10
+ )
11
+
12
+ DEFAULT_OPTIONS = {
13
+ format: :html,
14
+ attr_quote: "'",
15
+ }.freeze
16
+
17
+ def initialize(opts = {})
18
+ super(DEFAULT_OPTIONS.merge(opts))
19
+ end
20
+
21
+ use Parser
22
+ use Compiler
23
+ use Html
24
+ filter :Escapable
25
+ filter :ControlFlow
26
+ filter :MultiFlattener
27
+ filter :StaticMerger
28
+ use :Generator do
29
+ options[:generator].new(options.to_hash.reject {|k,v| !options[:generator].options.valid_key?(k) })
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,29 @@
1
+ require 'fast_haml/text_compiler'
2
+
3
+ module FastHaml
4
+ module FilterCompilers
5
+ class Base
6
+ protected
7
+
8
+ def compile_texts(temple, texts, tab_width: 0)
9
+ tabs = ' ' * tab_width
10
+ texts.each do |text|
11
+ temple << [:static, tabs] << text_compiler.compile(text) << [:static, "\n"]
12
+ end
13
+ nil
14
+ end
15
+
16
+ def text_compiler
17
+ @text_compiler ||= TextCompiler.new(escape_html: false)
18
+ end
19
+
20
+ def strip_last_empty_lines(texts)
21
+ texts = texts.dup
22
+ while texts.last && texts.last.empty?
23
+ texts.pop
24
+ end
25
+ texts
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,15 @@
1
+ require 'fast_haml/filter_compilers/base'
2
+
3
+ module FastHaml
4
+ module FilterCompilers
5
+ class Cdata < Base
6
+ def compile(texts)
7
+ temple = [:multi, [:static, "<![CDATA[\n"]]
8
+ compile_texts(temple, strip_last_empty_lines(texts), tab_width: 4)
9
+ temple << [:static, "]]>"]
10
+ end
11
+ end
12
+
13
+ register(:cdata, Cdata)
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ require 'fast_haml/filter_compilers/base'
2
+
3
+ module FastHaml
4
+ module FilterCompilers
5
+ class Css < Base
6
+ def compile(texts)
7
+ temple = [:multi, [:static, "\n"]]
8
+ compile_texts(temple, strip_last_empty_lines(texts), tab_width: 2)
9
+ [:haml, :tag, 'style', false, [:html, :attrs], temple]
10
+ end
11
+ end
12
+
13
+ register(:css, Css)
14
+ end
15
+ end
@@ -0,0 +1,22 @@
1
+ require 'fast_haml/filter_compilers/base'
2
+
3
+ module FastHaml
4
+ module FilterCompilers
5
+ class Escaped < Base
6
+ include Temple::Utils
7
+
8
+ def compile(texts)
9
+ temple = [:multi]
10
+ compile_texts(temple, strip_last_empty_lines(texts))
11
+ escape_code = Temple::Filters::Escapable.new.instance_variable_get(:@escape_code)
12
+ sym = unique_name
13
+ [:multi,
14
+ [:capture, sym, temple],
15
+ [:dynamic, escape_code % sym],
16
+ ]
17
+ end
18
+ end
19
+
20
+ register(:escaped, Escaped)
21
+ end
22
+ end
@@ -0,0 +1,15 @@
1
+ require 'fast_haml/filter_compilers/base'
2
+
3
+ module FastHaml
4
+ module FilterCompilers
5
+ class Javascript < Base
6
+ def compile(texts)
7
+ temple = [:multi, [:static, "\n"]]
8
+ compile_texts(temple, strip_last_empty_lines(texts), tab_width: 2)
9
+ [:haml, :tag, 'script', false, [:html, :attrs], [:html, :js, temple]]
10
+ end
11
+ end
12
+
13
+ register(:javascript, Javascript)
14
+ end
15
+ end
@@ -0,0 +1,17 @@
1
+ require 'fast_haml/filter_compilers/base'
2
+
3
+ module FastHaml
4
+ module FilterCompilers
5
+ class Plain < Base
6
+ def compile(texts)
7
+ temple = [:multi]
8
+ texts = strip_last_empty_lines(texts)
9
+ compile_texts(temple, texts[0 .. -2])
10
+ temple << text_compiler.compile(texts[-1])
11
+ temple
12
+ end
13
+ end
14
+
15
+ register(:plain, Plain)
16
+ end
17
+ end
@@ -0,0 +1,30 @@
1
+ require 'fast_haml/filter_compilers/base'
2
+
3
+ module FastHaml
4
+ module FilterCompilers
5
+ class Preserve < Base
6
+ include Temple::Utils
7
+
8
+ def compile(texts)
9
+ temple = [:multi]
10
+ texts.each do |text|
11
+ temple << text_compiler.compile(text)
12
+ unless texts.last.equal?(text)
13
+ temple << [:static, "\n"]
14
+ end
15
+ end
16
+ sym = unique_name
17
+ [:multi,
18
+ [:capture, sym, temple],
19
+ [:dynamic, "::FastHaml::FilterCompilers::Preserve.preserve(#{sym})"],
20
+ ]
21
+ end
22
+
23
+ def self.preserve(str)
24
+ str.gsub("\n", '&#x000A;')
25
+ end
26
+ end
27
+
28
+ register(:preserve, Preserve)
29
+ end
30
+ end
@@ -0,0 +1,13 @@
1
+ require 'fast_haml/filter_compilers/base'
2
+
3
+ module FastHaml
4
+ module FilterCompilers
5
+ class Ruby < Base
6
+ def compile(texts)
7
+ [:multi, [:code, strip_last_empty_lines(texts).join("\n")], [:newline]]
8
+ end
9
+ end
10
+
11
+ register(:ruby, Ruby)
12
+ end
13
+ end
@@ -0,0 +1,37 @@
1
+ module FastHaml
2
+ module FilterCompilers
3
+ class NotFound < StandardError
4
+ attr_reader
5
+
6
+ def initialize(name)
7
+ super("Unable to find compiler for #{name}")
8
+ @name = name
9
+ end
10
+ end
11
+
12
+ def self.compilers
13
+ @compilers ||= {}
14
+ end
15
+
16
+ def self.register(name, compiler)
17
+ compilers[name.to_s] = compiler
18
+ end
19
+
20
+ def self.find(name)
21
+ name = name.to_s
22
+ if compilers.has_key?(name.to_s)
23
+ compilers[name].new
24
+ else
25
+ raise NotFound.new(name)
26
+ end
27
+ end
28
+ end
29
+ end
30
+
31
+ require 'fast_haml/filter_compilers/cdata'
32
+ require 'fast_haml/filter_compilers/css'
33
+ require 'fast_haml/filter_compilers/escaped'
34
+ require 'fast_haml/filter_compilers/javascript'
35
+ require 'fast_haml/filter_compilers/plain'
36
+ require 'fast_haml/filter_compilers/preserve'
37
+ require 'fast_haml/filter_compilers/ruby'
@@ -0,0 +1,54 @@
1
+ module FastHaml
2
+ class FilterParser
3
+ def initialize(indent_tracker)
4
+ @ast = nil
5
+ @indent_level = nil
6
+ @indent_tracker = indent_tracker
7
+ end
8
+
9
+ def enabled?
10
+ !!@ast
11
+ end
12
+
13
+ def start(name)
14
+ @ast = Ast::Filter.new
15
+ @ast.name = name
16
+ end
17
+
18
+ def append(line)
19
+ indent, text = @indent_tracker.split(line)
20
+ if text.empty?
21
+ @ast.texts << ''
22
+ return
23
+ end
24
+ indent_level = indent.size
25
+
26
+ if @indent_level
27
+ if indent_level < @indent_level
28
+ # Finish filter
29
+ @indent_level = nil
30
+ ast = @ast
31
+ @ast = nil
32
+ return ast
33
+ end
34
+ else
35
+ if indent_level > @indent_tracker.current_level
36
+ # Start filter
37
+ @indent_level = indent_level
38
+ else
39
+ # Empty filter
40
+ @ast = nil
41
+ return nil
42
+ end
43
+ end
44
+
45
+ text = line[@indent_level .. -1]
46
+ @ast.texts << text
47
+ nil
48
+ end
49
+
50
+ def finish
51
+ @ast
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,44 @@
1
+ require 'fast_haml/attribute_builder'
2
+
3
+ module FastHaml
4
+ class Html < Temple::HTML::Fast
5
+ def on_haml_tag(name, self_closing, attrs, content = nil)
6
+ name = name.to_s
7
+ closed = self_closing && (!content || empty_exp?(content))
8
+ result = [:multi, [:static, "<#{name}"], compile(attrs)]
9
+ result << [:static, (closed && @format != :html ? ' /' : '') + '>']
10
+ result << compile(content) if content
11
+ result << [:static, "</#{name}>"] if !closed
12
+ result
13
+ end
14
+
15
+ def on_haml_attrs(code)
16
+ [:dynamic, "::FastHaml::AttributeBuilder.build(#{options[:attr_quote].inspect}, #{code})"]
17
+ end
18
+
19
+ def on_haml_attr(name, value)
20
+ if empty_exp?(value)
21
+ if @format == :html
22
+ [:static, " #{name}"]
23
+ else
24
+ [:static, " #{name}=#{options[:attr_quote]}#{name}#{options[:attr_quote]}"]
25
+ end
26
+ else
27
+ [:multi,
28
+ [:static, " #{name}=#{options[:attr_quote]}"],
29
+ compile(value),
30
+ [:static, options[:attr_quote]]]
31
+ end
32
+ end
33
+
34
+ def on_haml_doctype(type)
35
+ compile([:html, :doctype, type])
36
+ rescue Temple::FilterError
37
+ [:multi]
38
+ end
39
+
40
+ def on_haml_preserve(sym)
41
+ [:dynamic, "::FastHaml::Compiler.find_and_preserve(#{sym}.to_s)"]
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,84 @@
1
+ module FastHaml
2
+ class IndentTracker
3
+ class IndentMismatch < StandardError
4
+ attr_reader :lineno
5
+
6
+ def initialize(message, lineno)
7
+ super("#{message} at line #{lineno}")
8
+ @lineno = lineno
9
+ end
10
+ end
11
+
12
+ def initialize(on_enter: nil, on_leave: nil)
13
+ @indent_levels = [0]
14
+ @on_enter = on_enter || lambda { |level, text| }
15
+ @on_leave = on_leave || lambda { |level, text| }
16
+ @comment_level = nil
17
+ end
18
+
19
+ def process(line, lineno)
20
+ indent, text = split(line)
21
+ indent_level = indent.size
22
+
23
+ unless text.empty?
24
+ track(indent_level, text, lineno)
25
+ end
26
+ [text, indent]
27
+ end
28
+
29
+ def split(line)
30
+ m = line.match(/\A( *)(.*)\z/)
31
+ [m[1], m[2]]
32
+ end
33
+
34
+ def finish
35
+ indent_leave(0, '', -1)
36
+ end
37
+
38
+ def current_level
39
+ @indent_levels.last
40
+ end
41
+
42
+ def enter_comment!
43
+ @comment_level = @indent_levels[-2]
44
+ end
45
+
46
+ private
47
+
48
+ def track(indent_level, text, lineno)
49
+ if indent_level > @indent_levels.last
50
+ indent_enter(indent_level, text)
51
+ elsif indent_level < @indent_levels.last
52
+ indent_leave(indent_level, text, lineno)
53
+ end
54
+ end
55
+
56
+ def indent_enter(indent_level, text)
57
+ unless @comment_level
58
+ @indent_levels.push(indent_level)
59
+ @on_enter.call(indent_level, text)
60
+ end
61
+ end
62
+
63
+ def indent_leave(indent_level, text, lineno)
64
+ if @comment_level
65
+ if indent_level <= @comment_level
66
+ # finish comment mode
67
+ @comment_level = nil
68
+ else
69
+ # still in comment
70
+ return
71
+ end
72
+ end
73
+
74
+ while indent_level < @indent_levels.last
75
+ @indent_levels.pop
76
+ @on_leave.call(indent_level, text)
77
+ end
78
+
79
+ if indent_level != @indent_levels.last
80
+ raise IndentMismatch.new("Unexpected indent level: #{indent_level}: indent_level=#{@indent_levels}", lineno)
81
+ end
82
+ end
83
+ end
84
+ end