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