faml 0.2.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 (140) 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 +27 -0
  6. data/Appraisals +26 -0
  7. data/CHANGELOG.md +47 -0
  8. data/Gemfile +8 -0
  9. data/LICENSE.txt +22 -0
  10. data/README.md +115 -0
  11. data/Rakefile +28 -0
  12. data/benchmark/attribute_builder.haml +5 -0
  13. data/benchmark/rendering.rb +35 -0
  14. data/bin/faml +4 -0
  15. data/ext/attribute_builder/attribute_builder.c +261 -0
  16. data/ext/attribute_builder/extconf.rb +3 -0
  17. data/faml.gemspec +38 -0
  18. data/gemfiles/rails_4.0.gemfile +9 -0
  19. data/gemfiles/rails_4.1.gemfile +9 -0
  20. data/gemfiles/rails_4.2.gemfile +9 -0
  21. data/gemfiles/rails_edge.gemfile +10 -0
  22. data/haml_spec_test.rb +22 -0
  23. data/lib/faml.rb +10 -0
  24. data/lib/faml/ast.rb +112 -0
  25. data/lib/faml/cli.rb +38 -0
  26. data/lib/faml/compiler.rb +374 -0
  27. data/lib/faml/element_parser.rb +235 -0
  28. data/lib/faml/engine.rb +34 -0
  29. data/lib/faml/filter_compilers.rb +41 -0
  30. data/lib/faml/filter_compilers/base.rb +43 -0
  31. data/lib/faml/filter_compilers/cdata.rb +15 -0
  32. data/lib/faml/filter_compilers/coffee.rb +16 -0
  33. data/lib/faml/filter_compilers/css.rb +16 -0
  34. data/lib/faml/filter_compilers/escaped.rb +23 -0
  35. data/lib/faml/filter_compilers/javascript.rb +16 -0
  36. data/lib/faml/filter_compilers/markdown.rb +18 -0
  37. data/lib/faml/filter_compilers/plain.rb +15 -0
  38. data/lib/faml/filter_compilers/preserve.rb +26 -0
  39. data/lib/faml/filter_compilers/ruby.rb +17 -0
  40. data/lib/faml/filter_compilers/sass.rb +15 -0
  41. data/lib/faml/filter_compilers/scss.rb +16 -0
  42. data/lib/faml/filter_compilers/tilt_base.rb +34 -0
  43. data/lib/faml/filter_parser.rb +54 -0
  44. data/lib/faml/html.rb +58 -0
  45. data/lib/faml/indent_tracker.rb +84 -0
  46. data/lib/faml/line_parser.rb +66 -0
  47. data/lib/faml/newline.rb +30 -0
  48. data/lib/faml/parser.rb +211 -0
  49. data/lib/faml/parser_utils.rb +17 -0
  50. data/lib/faml/rails_handler.rb +10 -0
  51. data/lib/faml/railtie.rb +9 -0
  52. data/lib/faml/ruby_multiline.rb +23 -0
  53. data/lib/faml/script_parser.rb +84 -0
  54. data/lib/faml/static_hash_parser.rb +113 -0
  55. data/lib/faml/syntax_error.rb +10 -0
  56. data/lib/faml/text_compiler.rb +69 -0
  57. data/lib/faml/tilt.rb +17 -0
  58. data/lib/faml/version.rb +3 -0
  59. data/spec/compiler_newline_spec.rb +162 -0
  60. data/spec/rails/Rakefile +6 -0
  61. data/spec/rails/app/assets/images/.keep +0 -0
  62. data/spec/rails/app/assets/javascripts/application.js +13 -0
  63. data/spec/rails/app/assets/stylesheets/application.css +15 -0
  64. data/spec/rails/app/controllers/application_controller.rb +5 -0
  65. data/spec/rails/app/controllers/books_controller.rb +8 -0
  66. data/spec/rails/app/controllers/concerns/.keep +0 -0
  67. data/spec/rails/app/helpers/application_helper.rb +2 -0
  68. data/spec/rails/app/mailers/.keep +0 -0
  69. data/spec/rails/app/models/.keep +0 -0
  70. data/spec/rails/app/models/book.rb +9 -0
  71. data/spec/rails/app/models/concerns/.keep +0 -0
  72. data/spec/rails/app/views/books/hello.html.haml +2 -0
  73. data/spec/rails/app/views/books/with_capture.html.haml +4 -0
  74. data/spec/rails/app/views/books/with_variables.html.haml +4 -0
  75. data/spec/rails/app/views/layouts/application.html.haml +9 -0
  76. data/spec/rails/bin/bundle +3 -0
  77. data/spec/rails/bin/rails +4 -0
  78. data/spec/rails/bin/rake +4 -0
  79. data/spec/rails/bin/setup +29 -0
  80. data/spec/rails/config.ru +4 -0
  81. data/spec/rails/config/application.rb +12 -0
  82. data/spec/rails/config/boot.rb +3 -0
  83. data/spec/rails/config/database.yml +25 -0
  84. data/spec/rails/config/environment.rb +5 -0
  85. data/spec/rails/config/environments/development.rb +41 -0
  86. data/spec/rails/config/environments/production.rb +79 -0
  87. data/spec/rails/config/environments/test.rb +42 -0
  88. data/spec/rails/config/initializers/assets.rb +11 -0
  89. data/spec/rails/config/initializers/backtrace_silencers.rb +7 -0
  90. data/spec/rails/config/initializers/cookies_serializer.rb +3 -0
  91. data/spec/rails/config/initializers/filter_parameter_logging.rb +4 -0
  92. data/spec/rails/config/initializers/inflections.rb +16 -0
  93. data/spec/rails/config/initializers/mime_types.rb +4 -0
  94. data/spec/rails/config/initializers/secret_key_base.rb +6 -0
  95. data/spec/rails/config/initializers/session_store.rb +3 -0
  96. data/spec/rails/config/initializers/wrap_parameters.rb +14 -0
  97. data/spec/rails/config/locales/en.yml +23 -0
  98. data/spec/rails/config/routes.rb +7 -0
  99. data/spec/rails/config/secrets.yml +22 -0
  100. data/spec/rails/db/seeds.rb +7 -0
  101. data/spec/rails/lib/assets/.keep +0 -0
  102. data/spec/rails/lib/tasks/.keep +0 -0
  103. data/spec/rails/log/.keep +0 -0
  104. data/spec/rails/public/404.html +67 -0
  105. data/spec/rails/public/422.html +67 -0
  106. data/spec/rails/public/500.html +66 -0
  107. data/spec/rails/public/favicon.ico +0 -0
  108. data/spec/rails/public/robots.txt +5 -0
  109. data/spec/rails/spec/requests/faml_spec.rb +41 -0
  110. data/spec/rails/vendor/assets/javascripts/.keep +0 -0
  111. data/spec/rails/vendor/assets/stylesheets/.keep +0 -0
  112. data/spec/rails_helper.rb +4 -0
  113. data/spec/render/attribute_spec.rb +241 -0
  114. data/spec/render/comment_spec.rb +61 -0
  115. data/spec/render/doctype_spec.rb +57 -0
  116. data/spec/render/element_spec.rb +136 -0
  117. data/spec/render/filters/cdata_spec.rb +12 -0
  118. data/spec/render/filters/coffee_spec.rb +25 -0
  119. data/spec/render/filters/css_spec.rb +45 -0
  120. data/spec/render/filters/escaped_spec.rb +14 -0
  121. data/spec/render/filters/javascript_spec.rb +44 -0
  122. data/spec/render/filters/markdown_spec.rb +19 -0
  123. data/spec/render/filters/plain_spec.rb +24 -0
  124. data/spec/render/filters/preserve_spec.rb +24 -0
  125. data/spec/render/filters/ruby_spec.rb +13 -0
  126. data/spec/render/filters/sass_spec.rb +28 -0
  127. data/spec/render/filters/scss_spec.rb +32 -0
  128. data/spec/render/filters_spec.rb +11 -0
  129. data/spec/render/haml_comment_spec.rb +24 -0
  130. data/spec/render/multiline_spec.rb +39 -0
  131. data/spec/render/newline_spec.rb +83 -0
  132. data/spec/render/plain_spec.rb +20 -0
  133. data/spec/render/preserve_spec.rb +8 -0
  134. data/spec/render/sanitize_spec.rb +36 -0
  135. data/spec/render/script_spec.rb +81 -0
  136. data/spec/render/silent_script_spec.rb +97 -0
  137. data/spec/render/unescape_spec.rb +45 -0
  138. data/spec/spec_helper.rb +47 -0
  139. data/spec/tilt_spec.rb +33 -0
  140. metadata +489 -0
@@ -0,0 +1,211 @@
1
+ require 'faml/ast'
2
+ require 'faml/element_parser'
3
+ require 'faml/filter_parser'
4
+ require 'faml/indent_tracker'
5
+ require 'faml/line_parser'
6
+ require 'faml/parser_utils'
7
+ require 'faml/ruby_multiline'
8
+ require 'faml/script_parser'
9
+ require 'faml/syntax_error'
10
+
11
+ module Faml
12
+ class Parser
13
+ def initialize(options = {})
14
+ end
15
+
16
+ def call(template_str)
17
+ @ast = Ast::Root.new
18
+ @stack = []
19
+ @line_parser = LineParser.new(template_str)
20
+ @indent_tracker = IndentTracker.new(on_enter: method(:indent_enter), on_leave: method(:indent_leave))
21
+ @filter_parser = FilterParser.new(@indent_tracker)
22
+
23
+ while @line_parser.has_next?
24
+ line = @line_parser.next_line
25
+ if !@ast.is_a?(Ast::HamlComment) && @filter_parser.enabled?
26
+ ast = @filter_parser.append(line)
27
+ if ast
28
+ @ast << ast
29
+ end
30
+ end
31
+ unless @filter_parser.enabled?
32
+ parse_line(line)
33
+ end
34
+ end
35
+
36
+ ast = @filter_parser.finish
37
+ if ast
38
+ @ast << ast
39
+ end
40
+ @indent_tracker.finish
41
+ @ast
42
+ end
43
+
44
+ private
45
+
46
+ DOCTYPE_PREFIX = '!'
47
+ ELEMENT_PREFIX = '%'
48
+ COMMENT_PREFIX = '/'
49
+ SILENT_SCRIPT_PREFIX = '-'
50
+ DIV_ID_PREFIX = '#'
51
+ DIV_CLASS_PREFIX = '.'
52
+ FILTER_PREFIX = ':'
53
+ ESCAPE_PREFIX = '\\'
54
+
55
+ def parse_line(line)
56
+ text, indent = @indent_tracker.process(line, @line_parser.lineno)
57
+
58
+ if text.empty?
59
+ @ast << Ast::Empty.new
60
+ return
61
+ end
62
+
63
+ if @ast.is_a?(Ast::HamlComment)
64
+ @ast << Ast::Text.new(text)
65
+ return
66
+ end
67
+
68
+ case text[0]
69
+ when ESCAPE_PREFIX
70
+ parse_plain(text[1 .. -1])
71
+ when ELEMENT_PREFIX
72
+ parse_element(text)
73
+ when DOCTYPE_PREFIX
74
+ if text.start_with?('!!!')
75
+ parse_doctype(text)
76
+ else
77
+ parse_script(text)
78
+ end
79
+ when COMMENT_PREFIX
80
+ parse_comment(text)
81
+ when SILENT_SCRIPT_PREFIX
82
+ parse_silent_script(text)
83
+ when DIV_ID_PREFIX, DIV_CLASS_PREFIX
84
+ if text.start_with?('#{')
85
+ parse_script(text)
86
+ else
87
+ parse_line("#{indent}%div#{text}")
88
+ end
89
+ when FILTER_PREFIX
90
+ parse_filter(text)
91
+ else
92
+ parse_script(text)
93
+ end
94
+ end
95
+
96
+ def parse_doctype(text)
97
+ @ast << Ast::Doctype.new(text[3 .. -1].strip)
98
+ end
99
+
100
+ def parse_comment(text)
101
+ text = text[1, text.size-1].strip
102
+ comment = Ast::HtmlComment.new
103
+ comment.comment = text
104
+ if text[0] == '['
105
+ comment.conditional, rest = parse_conditional_comment(text)
106
+ text.replace(rest)
107
+ end
108
+ @ast << comment
109
+ end
110
+
111
+ CONDITIONAL_COMMENT_REGEX = /[\[\]]/o
112
+
113
+ def parse_conditional_comment(text)
114
+ s = StringScanner.new(text[1 .. -1])
115
+ depth = ParserUtils.balance(s, '[', ']')
116
+ if depth == 0
117
+ [s.pre_match, s.rest.lstrip]
118
+ else
119
+ syntax_error!('Unmatched brackets in conditional comment')
120
+ end
121
+ end
122
+
123
+ def parse_plain(text)
124
+ @ast << Ast::Text.new(text)
125
+ end
126
+
127
+ def parse_element(text)
128
+ @ast << ElementParser.new(@line_parser).parse(text)
129
+ end
130
+
131
+ def parse_script(text)
132
+ @ast << ScriptParser.new(@line_parser).parse(text)
133
+ end
134
+
135
+ def parse_silent_script(text)
136
+ if text.start_with?('-#')
137
+ @ast << Ast::HamlComment.new
138
+ return
139
+ end
140
+ script = text[/\A- *(.*)\z/, 1]
141
+ if script.empty?
142
+ syntax_error!("No Ruby code to evaluate")
143
+ end
144
+ script += RubyMultiline.read(@line_parser, script)
145
+ @ast << Ast::SilentScript.new([], script)
146
+ end
147
+
148
+ def parse_filter(text)
149
+ filter_name = text[/\A#{FILTER_PREFIX}(\w+)\z/, 1]
150
+ unless filter_name
151
+ syntax_error!("Invalid filter name: #{text}")
152
+ end
153
+ @filter_parser.start(filter_name)
154
+ end
155
+
156
+ def indent_enter(_, text)
157
+ empty_lines = []
158
+ while @ast.children.last.is_a?(Ast::Empty)
159
+ empty_lines << @ast.children.pop
160
+ end
161
+ @stack.push(@ast)
162
+ @ast = @ast.children.last
163
+ @ast.children = empty_lines
164
+ if @ast.is_a?(Ast::Element) && @ast.self_closing
165
+ syntax_error!('Illegal nesting: nesting within a self-closing tag is illegal')
166
+ end
167
+ if @ast.is_a?(Ast::HtmlComment) && !@ast.comment.empty?
168
+ syntax_error!('Illegal nesting: nesting within a html comment that already has content is illegal.')
169
+ end
170
+ if @ast.is_a?(Ast::HamlComment)
171
+ @indent_tracker.enter_comment!
172
+ end
173
+ nil
174
+ end
175
+
176
+ def indent_leave(indent_level, text)
177
+ parent_ast = @stack.pop
178
+ case @ast
179
+ when Ast::Script, Ast::SilentScript
180
+ if indent_level == @indent_tracker.current_level
181
+ @ast.mid_block_keyword = mid_block_keyword?(text)
182
+ end
183
+ end
184
+ @ast = parent_ast
185
+ nil
186
+ end
187
+
188
+ MID_BLOCK_KEYWORDS = %w[else elsif rescue ensure end when]
189
+ START_BLOCK_KEYWORDS = %w[if begin case unless]
190
+ # Try to parse assignments to block starters as best as possible
191
+ START_BLOCK_KEYWORD_REGEX = /(?:\w+(?:,\s*\w+)*\s*=\s*)?(#{Regexp.union(START_BLOCK_KEYWORDS)})/
192
+ BLOCK_KEYWORD_REGEX = /^-?\s*(?:(#{Regexp.union(MID_BLOCK_KEYWORDS)})|#{START_BLOCK_KEYWORD_REGEX.source})\b/
193
+
194
+ def block_keyword(text)
195
+ m = text.match(BLOCK_KEYWORD_REGEX)
196
+ if m
197
+ m[1] || m[2]
198
+ else
199
+ nil
200
+ end
201
+ end
202
+
203
+ def mid_block_keyword?(text)
204
+ MID_BLOCK_KEYWORDS.include?(block_keyword(text))
205
+ end
206
+
207
+ def syntax_error!(message)
208
+ raise SyntaxError.new(message, @line_parser.lineno)
209
+ end
210
+ end
211
+ end
@@ -0,0 +1,17 @@
1
+ module Faml
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 Faml
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,9 @@
1
+ module Faml
2
+ class Railtie < ::Rails::Railtie
3
+ initializer :faml do |app|
4
+ require 'faml/rails_handler'
5
+ ActionView::Template.register_template_handler(:haml, Faml::RailsHandler.new)
6
+ ActionView::Template.register_template_handler(:faml, Faml::RailsHandler.new)
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,23 @@
1
+ module Faml
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,84 @@
1
+ require 'faml/ast'
2
+ require 'faml/ruby_multiline'
3
+ require 'faml/syntax_error'
4
+
5
+ module Faml
6
+ class ScriptParser
7
+ def initialize(line_parser)
8
+ @line_parser = line_parser
9
+ end
10
+
11
+ def parse(text)
12
+ case text[0]
13
+ when '=', '~'
14
+ parse_script(text)
15
+ when '&'
16
+ parse_sanitized(text)
17
+ when '!'
18
+ parse_unescape(text)
19
+ else
20
+ parse_text(text)
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ def parse_script(text)
27
+ if text[1] == '='
28
+ Ast::Text.new(text[2 .. -1].strip)
29
+ else
30
+ script = text[1 .. -1].lstrip
31
+ if script.empty?
32
+ syntax_error!('No Ruby code to evaluate')
33
+ end
34
+ script += RubyMultiline.read(@line_parser, script)
35
+ Ast::Script.new([], script)
36
+ end
37
+ end
38
+
39
+ def parse_sanitized(text)
40
+ case
41
+ when text.start_with?('&==')
42
+ Ast::Text.new(text[3 .. -1].lstrip)
43
+ when text[1] == '=' || text[1] == '~'
44
+ script = text[2 .. -1].lstrip
45
+ if script.empty?
46
+ syntax_error!('No Ruby code to evaluate')
47
+ end
48
+ script += RubyMultiline.read(@line_parser, script)
49
+ Ast::Script.new([], script, true, text[1] == '~')
50
+ else
51
+ Ast::Text.new(text[1 .. -1].strip)
52
+ end
53
+ end
54
+
55
+ def parse_unescape(text)
56
+ case
57
+ when text.start_with?('!==')
58
+ Ast::Text.new(text[3 .. -1].lstrip, false)
59
+ when text[1] == '=' || text[1] == '~'
60
+ script = text[2 .. -1].lstrip
61
+ if script.empty?
62
+ syntax_error!('No Ruby code to evaluate')
63
+ end
64
+ script += RubyMultiline.read(@line_parser, script)
65
+ Ast::Script.new([], script, false, text[1] == '~')
66
+ else
67
+ Ast::Text.new(text[1 .. -1].lstrip, false)
68
+ end
69
+ end
70
+
71
+ def parse_text(text)
72
+ text = text.lstrip
73
+ if text.empty?
74
+ nil
75
+ else
76
+ Ast::Text.new(text)
77
+ end
78
+ end
79
+
80
+ def syntax_error!(message)
81
+ raise SyntaxError.new(message, @line_parser.lineno)
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,113 @@
1
+ require 'parser/current'
2
+
3
+ module Faml
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('(faml)')
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