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