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.
- checksums.yaml +7 -0
- data/.gitignore +16 -0
- data/.gitmodules +3 -0
- data/.rspec +2 -0
- data/.travis.yml +27 -0
- data/Appraisals +26 -0
- data/CHANGELOG.md +47 -0
- data/Gemfile +8 -0
- data/LICENSE.txt +22 -0
- data/README.md +115 -0
- data/Rakefile +28 -0
- data/benchmark/attribute_builder.haml +5 -0
- data/benchmark/rendering.rb +35 -0
- data/bin/faml +4 -0
- data/ext/attribute_builder/attribute_builder.c +261 -0
- data/ext/attribute_builder/extconf.rb +3 -0
- data/faml.gemspec +38 -0
- data/gemfiles/rails_4.0.gemfile +9 -0
- data/gemfiles/rails_4.1.gemfile +9 -0
- data/gemfiles/rails_4.2.gemfile +9 -0
- data/gemfiles/rails_edge.gemfile +10 -0
- data/haml_spec_test.rb +22 -0
- data/lib/faml.rb +10 -0
- data/lib/faml/ast.rb +112 -0
- data/lib/faml/cli.rb +38 -0
- data/lib/faml/compiler.rb +374 -0
- data/lib/faml/element_parser.rb +235 -0
- data/lib/faml/engine.rb +34 -0
- data/lib/faml/filter_compilers.rb +41 -0
- data/lib/faml/filter_compilers/base.rb +43 -0
- data/lib/faml/filter_compilers/cdata.rb +15 -0
- data/lib/faml/filter_compilers/coffee.rb +16 -0
- data/lib/faml/filter_compilers/css.rb +16 -0
- data/lib/faml/filter_compilers/escaped.rb +23 -0
- data/lib/faml/filter_compilers/javascript.rb +16 -0
- data/lib/faml/filter_compilers/markdown.rb +18 -0
- data/lib/faml/filter_compilers/plain.rb +15 -0
- data/lib/faml/filter_compilers/preserve.rb +26 -0
- data/lib/faml/filter_compilers/ruby.rb +17 -0
- data/lib/faml/filter_compilers/sass.rb +15 -0
- data/lib/faml/filter_compilers/scss.rb +16 -0
- data/lib/faml/filter_compilers/tilt_base.rb +34 -0
- data/lib/faml/filter_parser.rb +54 -0
- data/lib/faml/html.rb +58 -0
- data/lib/faml/indent_tracker.rb +84 -0
- data/lib/faml/line_parser.rb +66 -0
- data/lib/faml/newline.rb +30 -0
- data/lib/faml/parser.rb +211 -0
- data/lib/faml/parser_utils.rb +17 -0
- data/lib/faml/rails_handler.rb +10 -0
- data/lib/faml/railtie.rb +9 -0
- data/lib/faml/ruby_multiline.rb +23 -0
- data/lib/faml/script_parser.rb +84 -0
- data/lib/faml/static_hash_parser.rb +113 -0
- data/lib/faml/syntax_error.rb +10 -0
- data/lib/faml/text_compiler.rb +69 -0
- data/lib/faml/tilt.rb +17 -0
- data/lib/faml/version.rb +3 -0
- data/spec/compiler_newline_spec.rb +162 -0
- data/spec/rails/Rakefile +6 -0
- data/spec/rails/app/assets/images/.keep +0 -0
- data/spec/rails/app/assets/javascripts/application.js +13 -0
- data/spec/rails/app/assets/stylesheets/application.css +15 -0
- data/spec/rails/app/controllers/application_controller.rb +5 -0
- data/spec/rails/app/controllers/books_controller.rb +8 -0
- data/spec/rails/app/controllers/concerns/.keep +0 -0
- data/spec/rails/app/helpers/application_helper.rb +2 -0
- data/spec/rails/app/mailers/.keep +0 -0
- data/spec/rails/app/models/.keep +0 -0
- data/spec/rails/app/models/book.rb +9 -0
- data/spec/rails/app/models/concerns/.keep +0 -0
- data/spec/rails/app/views/books/hello.html.haml +2 -0
- data/spec/rails/app/views/books/with_capture.html.haml +4 -0
- data/spec/rails/app/views/books/with_variables.html.haml +4 -0
- data/spec/rails/app/views/layouts/application.html.haml +9 -0
- data/spec/rails/bin/bundle +3 -0
- data/spec/rails/bin/rails +4 -0
- data/spec/rails/bin/rake +4 -0
- data/spec/rails/bin/setup +29 -0
- data/spec/rails/config.ru +4 -0
- data/spec/rails/config/application.rb +12 -0
- data/spec/rails/config/boot.rb +3 -0
- data/spec/rails/config/database.yml +25 -0
- data/spec/rails/config/environment.rb +5 -0
- data/spec/rails/config/environments/development.rb +41 -0
- data/spec/rails/config/environments/production.rb +79 -0
- data/spec/rails/config/environments/test.rb +42 -0
- data/spec/rails/config/initializers/assets.rb +11 -0
- data/spec/rails/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/rails/config/initializers/cookies_serializer.rb +3 -0
- data/spec/rails/config/initializers/filter_parameter_logging.rb +4 -0
- data/spec/rails/config/initializers/inflections.rb +16 -0
- data/spec/rails/config/initializers/mime_types.rb +4 -0
- data/spec/rails/config/initializers/secret_key_base.rb +6 -0
- data/spec/rails/config/initializers/session_store.rb +3 -0
- data/spec/rails/config/initializers/wrap_parameters.rb +14 -0
- data/spec/rails/config/locales/en.yml +23 -0
- data/spec/rails/config/routes.rb +7 -0
- data/spec/rails/config/secrets.yml +22 -0
- data/spec/rails/db/seeds.rb +7 -0
- data/spec/rails/lib/assets/.keep +0 -0
- data/spec/rails/lib/tasks/.keep +0 -0
- data/spec/rails/log/.keep +0 -0
- data/spec/rails/public/404.html +67 -0
- data/spec/rails/public/422.html +67 -0
- data/spec/rails/public/500.html +66 -0
- data/spec/rails/public/favicon.ico +0 -0
- data/spec/rails/public/robots.txt +5 -0
- data/spec/rails/spec/requests/faml_spec.rb +41 -0
- data/spec/rails/vendor/assets/javascripts/.keep +0 -0
- data/spec/rails/vendor/assets/stylesheets/.keep +0 -0
- data/spec/rails_helper.rb +4 -0
- data/spec/render/attribute_spec.rb +241 -0
- data/spec/render/comment_spec.rb +61 -0
- data/spec/render/doctype_spec.rb +57 -0
- data/spec/render/element_spec.rb +136 -0
- data/spec/render/filters/cdata_spec.rb +12 -0
- data/spec/render/filters/coffee_spec.rb +25 -0
- data/spec/render/filters/css_spec.rb +45 -0
- data/spec/render/filters/escaped_spec.rb +14 -0
- data/spec/render/filters/javascript_spec.rb +44 -0
- data/spec/render/filters/markdown_spec.rb +19 -0
- data/spec/render/filters/plain_spec.rb +24 -0
- data/spec/render/filters/preserve_spec.rb +24 -0
- data/spec/render/filters/ruby_spec.rb +13 -0
- data/spec/render/filters/sass_spec.rb +28 -0
- data/spec/render/filters/scss_spec.rb +32 -0
- data/spec/render/filters_spec.rb +11 -0
- data/spec/render/haml_comment_spec.rb +24 -0
- data/spec/render/multiline_spec.rb +39 -0
- data/spec/render/newline_spec.rb +83 -0
- data/spec/render/plain_spec.rb +20 -0
- data/spec/render/preserve_spec.rb +8 -0
- data/spec/render/sanitize_spec.rb +36 -0
- data/spec/render/script_spec.rb +81 -0
- data/spec/render/silent_script_spec.rb +97 -0
- data/spec/render/unescape_spec.rb +45 -0
- data/spec/spec_helper.rb +47 -0
- data/spec/tilt_spec.rb +33 -0
- metadata +489 -0
data/lib/faml/parser.rb
ADDED
|
@@ -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
|
data/lib/faml/railtie.rb
ADDED
|
@@ -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
|