faml 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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,235 @@
1
+ require 'strscan'
2
+ require 'faml/ast'
3
+ require 'faml/parser_utils'
4
+ require 'faml/ruby_multiline'
5
+ require 'faml/script_parser'
6
+ require 'faml/syntax_error'
7
+
8
+ module Faml
9
+ class ElementParser
10
+ def initialize(line_parser)
11
+ @line_parser = line_parser
12
+ end
13
+
14
+ ELEMENT_REGEXP = /\A%([-:\w]+)([-:\w.#]*)(.+)?\z/o
15
+
16
+ def parse(text)
17
+ m = text.match(ELEMENT_REGEXP)
18
+ unless m
19
+ syntax_error!('Invalid element declaration')
20
+ end
21
+
22
+ element = Ast::Element.new
23
+ element.tag_name = m[1]
24
+ element.static_class, element.static_id = parse_class_and_id(m[2])
25
+ rest = m[3] || ''
26
+
27
+ element.attributes, rest = parse_attributes(rest)
28
+ element.nuke_inner_whitespace, element.nuke_outer_whitespace, rest = parse_nuke_whitespace(rest)
29
+ element.self_closing, rest = parse_self_closing(rest)
30
+ element.oneline_child = ScriptParser.new(@line_parser).parse(rest)
31
+
32
+ element
33
+ end
34
+
35
+ private
36
+
37
+ def parse_class_and_id(class_and_id)
38
+ classes = []
39
+ id = ''
40
+ class_and_id.scan(/([#.])([-:_a-zA-Z0-9]+)/) do |type, prop|
41
+ case type
42
+ when '.'
43
+ classes << prop
44
+ when '#'
45
+ id = prop
46
+ end
47
+ end
48
+
49
+ [classes.join(' '), id]
50
+ end
51
+
52
+ OLD_ATTRIBUTE_BEGIN = '{'
53
+ NEW_ATTRIBUTE_BEGIN = '('
54
+
55
+ def parse_attributes(rest)
56
+ old_attributes = ''
57
+ new_attributes = ''
58
+
59
+ loop do
60
+ case rest[0]
61
+ when OLD_ATTRIBUTE_BEGIN
62
+ unless old_attributes.empty?
63
+ break
64
+ end
65
+ old_attributes, rest = parse_old_attributes(rest)
66
+ when NEW_ATTRIBUTE_BEGIN
67
+ unless new_attributes.empty?
68
+ break
69
+ end
70
+ new_attributes, rest = parse_new_attributes(rest)
71
+ else
72
+ break
73
+ end
74
+ end
75
+
76
+ attributes = old_attributes
77
+ unless new_attributes.empty?
78
+ t = to_old_syntax(new_attributes)
79
+ if attributes.empty?
80
+ attributes = t
81
+ else
82
+ attributes << ", " << t
83
+ end
84
+ end
85
+ [attributes, rest]
86
+ end
87
+
88
+ def parse_old_attributes(text)
89
+ text = text.dup
90
+ s = StringScanner.new(text)
91
+ s.pos = 1
92
+ depth = 1
93
+ loop do
94
+ depth = ParserUtils.balance(s, '{', '}', depth)
95
+ if depth == 0
96
+ attr = s.pre_match + s.matched
97
+ return [attr[1, attr.size-2], s.rest.lstrip]
98
+ else
99
+ if text[-1] == ',' && @line_parser.has_next?
100
+ text << @line_parser.next_line
101
+ else
102
+ syntax_error!('Unmatched brace')
103
+ end
104
+ end
105
+ end
106
+ end
107
+
108
+ def parse_new_attributes(text)
109
+ text = text.dup
110
+ s = StringScanner.new(text)
111
+ s.pos = 1
112
+ depth = 1
113
+ new_attributes = []
114
+ loop do
115
+ pre_pos = s.pos
116
+ depth = ParserUtils.balance(s, '(', ')', depth)
117
+ if depth == 0
118
+ t = s.string.byteslice(pre_pos ... s.pos-1)
119
+ new_attributes.concat(parse_new_attribute_list(t))
120
+ return [new_attributes, s.rest.lstrip]
121
+ else
122
+ if @line_parser.has_next?
123
+ text << ' ' << @line_parser.next_line
124
+ else
125
+ syntax_error!('Unmatched paren')
126
+ end
127
+ end
128
+ end
129
+ end
130
+
131
+ def parse_new_attribute_list(text)
132
+ s = StringScanner.new(text)
133
+ list = []
134
+ until s.eos?
135
+ name = scan_key(s)
136
+ s.skip(/\s*/)
137
+
138
+ if scan_operator(s)
139
+ s.skip(/\s*/)
140
+ value = scan_value(s)
141
+ else
142
+ value = 'true'
143
+ end
144
+ s.skip(/\s*/)
145
+
146
+ list << [name, value]
147
+ end
148
+ list
149
+ end
150
+
151
+ def scan_key(scanner)
152
+ scanner.scan(/[-:\w]+/).tap do |name|
153
+ unless name
154
+ syntax_error!('Invalid attribute list (missing attributename)')
155
+ end
156
+ end
157
+ end
158
+
159
+ def scan_operator(scanner)
160
+ scanner.skip(/=/)
161
+ end
162
+
163
+ def scan_value(scanner)
164
+ if quote = scanner.scan(/["']/)
165
+ scan_quoted_value(scanner, quote)
166
+ else
167
+ scan_variable_value(scanner)
168
+ end
169
+ end
170
+
171
+ def scan_quoted_value(scanner, quote)
172
+ re = /((?:\\.|\#(?!\{)|[^#{quote}\\#])*)(#{quote}|#\{)/
173
+ pos = scanner.pos
174
+ loop do
175
+ unless scanner.scan(re)
176
+ syntax_error!('Invalid attribute list (mismatched quotation)')
177
+ end
178
+ if scanner[2] == quote
179
+ break
180
+ end
181
+ depth = ParserUtils.balance(scanner, '{', '}')
182
+ if depth != 0
183
+ syntax_error!('Invalid attribute list (mismatched interpolation)')
184
+ end
185
+ end
186
+ str = scanner.string.byteslice(pos-1 .. scanner.pos-1)
187
+
188
+ # Even if the quote is single, string interpolation is performed in Haml.
189
+ str[0] = '"'
190
+ str[-1] = '"'
191
+ str
192
+ end
193
+
194
+ def scan_variable_value(scanner)
195
+ scanner.scan(/(@@?|\$)?\w+/).tap do |var|
196
+ unless var
197
+ syntax_error!('Invalid attribute list (invalid variable name)')
198
+ end
199
+ end
200
+ end
201
+
202
+ def to_old_syntax(new_attributes)
203
+ new_attributes.map { |k, v| "#{k.inspect} => #{v}" }.join(', ')
204
+ end
205
+
206
+ def parse_nuke_whitespace(rest)
207
+ m = rest.match(/\A(><|<>|[><])(.*)\z/)
208
+ if m
209
+ nuke_whitespace = m[1]
210
+ [
211
+ nuke_whitespace.include?('<'),
212
+ nuke_whitespace.include?('>'),
213
+ m[2],
214
+ ]
215
+ else
216
+ [false, false, rest]
217
+ end
218
+ end
219
+
220
+ def parse_self_closing(rest)
221
+ if rest[0] == '/'
222
+ if rest.size > 1
223
+ syntax_error!("Self-closing tags can't have content")
224
+ end
225
+ [true, '']
226
+ else
227
+ [false, rest]
228
+ end
229
+ end
230
+
231
+ def syntax_error!(message)
232
+ raise SyntaxError.new(message, @line_parser.lineno)
233
+ end
234
+ end
235
+ end
@@ -0,0 +1,34 @@
1
+ require 'temple'
2
+ require 'faml/compiler'
3
+ require 'faml/html'
4
+ require 'faml/newline'
5
+ require 'faml/parser'
6
+
7
+ module Faml
8
+ class Engine < Temple::Engine
9
+ define_options(
10
+ generator: Temple::Generators::ArrayBuffer,
11
+ )
12
+
13
+ DEFAULT_OPTIONS = {
14
+ format: :html,
15
+ attr_quote: "'",
16
+ }.freeze
17
+
18
+ def initialize(opts = {})
19
+ super(DEFAULT_OPTIONS.merge(opts))
20
+ end
21
+
22
+ use Parser
23
+ use Compiler
24
+ use Html
25
+ filter :Escapable
26
+ filter :ControlFlow
27
+ filter :MultiFlattener
28
+ use Newline
29
+ filter :StaticMerger
30
+ use :Generator do
31
+ options[:generator].new(options.to_hash.reject {|k,v| !options[:generator].options.valid_key?(k) })
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,41 @@
1
+ module Faml
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 'faml/filter_compilers/cdata'
32
+ require 'faml/filter_compilers/coffee'
33
+ require 'faml/filter_compilers/css'
34
+ require 'faml/filter_compilers/escaped'
35
+ require 'faml/filter_compilers/javascript'
36
+ require 'faml/filter_compilers/markdown'
37
+ require 'faml/filter_compilers/plain'
38
+ require 'faml/filter_compilers/preserve'
39
+ require 'faml/filter_compilers/ruby'
40
+ require 'faml/filter_compilers/sass'
41
+ require 'faml/filter_compilers/scss'
@@ -0,0 +1,43 @@
1
+ require 'faml/text_compiler'
2
+
3
+ module Faml
4
+ module FilterCompilers
5
+ class Base
6
+ def need_newline?
7
+ true
8
+ end
9
+
10
+ protected
11
+
12
+ def compile_texts(temple, texts, tab_width: 0, keep_last_empty_lines: false)
13
+ tabs = ' ' * tab_width
14
+ n = 0
15
+ unless keep_last_empty_lines
16
+ texts, n = strip_last_empty_lines(texts)
17
+ end
18
+ texts.each do |text|
19
+ temple << [:static, tabs] << text_compiler.compile(text)
20
+ unless texts.last.equal?(text)
21
+ temple << [:static, "\n"] << [:newline]
22
+ end
23
+ end
24
+ temple.concat([[:newline]] * n)
25
+ nil
26
+ end
27
+
28
+ def text_compiler
29
+ @text_compiler ||= TextCompiler.new(escape_html: false)
30
+ end
31
+
32
+ def strip_last_empty_lines(texts)
33
+ n = 0
34
+ texts = texts.dup
35
+ while texts.last && texts.last.empty?
36
+ n += 1
37
+ texts.pop
38
+ end
39
+ [texts, n]
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,15 @@
1
+ require 'faml/filter_compilers/base'
2
+
3
+ module Faml
4
+ module FilterCompilers
5
+ class Cdata < Base
6
+ def compile(texts)
7
+ temple = [:multi, [:static, "<![CDATA[\n"], [:newline]]
8
+ compile_texts(temple, texts, tab_width: 4)
9
+ temple << [:static, "\n]]>"]
10
+ end
11
+ end
12
+
13
+ register(:cdata, Cdata)
14
+ end
15
+ end
@@ -0,0 +1,16 @@
1
+ require 'faml/filter_compilers/tilt_base'
2
+
3
+ module Faml
4
+ module FilterCompilers
5
+ class Coffee < TiltBase
6
+ def compile(texts)
7
+ temple = [:multi, [:static, "\n"], [:newline]]
8
+ compile_with_tilt(temple, 'coffee', texts)
9
+ temple << [:static, "\n"]
10
+ [:haml, :tag, 'script', false, [:html, :attrs], [:html, :js, temple]]
11
+ end
12
+ end
13
+
14
+ register(:coffee, Coffee)
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ require 'faml/filter_compilers/base'
2
+
3
+ module Faml
4
+ module FilterCompilers
5
+ class Css < Base
6
+ def compile(texts)
7
+ temple = [:multi, [:static, "\n"], [:newline]]
8
+ compile_texts(temple, texts, tab_width: 2)
9
+ temple << [:static, "\n"]
10
+ [:haml, :tag, 'style', false, [:html, :attrs], temple]
11
+ end
12
+ end
13
+
14
+ register(:css, Css)
15
+ end
16
+ end
@@ -0,0 +1,23 @@
1
+ require 'faml/filter_compilers/base'
2
+
3
+ module Faml
4
+ module FilterCompilers
5
+ class Escaped < Base
6
+ include Temple::Utils
7
+
8
+ def compile(texts)
9
+ temple = [:multi, [:newline]]
10
+ compile_texts(temple, texts)
11
+ temple << [:static, "\n"]
12
+ escape_code = Temple::Filters::Escapable.new.instance_variable_get(:@escape_code)
13
+ sym = unique_name
14
+ [:multi,
15
+ [:capture, sym, temple],
16
+ [:dynamic, escape_code % sym],
17
+ ]
18
+ end
19
+ end
20
+
21
+ register(:escaped, Escaped)
22
+ end
23
+ end
@@ -0,0 +1,16 @@
1
+ require 'faml/filter_compilers/base'
2
+
3
+ module Faml
4
+ module FilterCompilers
5
+ class Javascript < Base
6
+ def compile(texts)
7
+ temple = [:multi, [:static, "\n"], [:newline]]
8
+ compile_texts(temple, texts, tab_width: 2)
9
+ temple << [:static, "\n"]
10
+ [:haml, :tag, 'script', false, [:html, :attrs], [:html, :js, temple]]
11
+ end
12
+ end
13
+
14
+ register(:javascript, Javascript)
15
+ end
16
+ end