bade 0.1.4 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,16 +1,363 @@
1
- require_relative 'node'
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'runtime'
4
+
5
+ require_relative 'ast/document'
2
6
 
3
7
  module Bade
4
8
  class Generator
5
- require_relative 'generator/html_generator'
6
- require_relative 'generator/ruby_generator'
9
+ BUFF_NAME = :__buff
10
+ MIXINS_NAME = :__mixins
11
+ NEW_LINE_NAME = :__new_line
12
+ CURRENT_INDENT_NAME = :__indent
13
+ BASE_INDENT_NAME = :__base_indent
14
+
15
+ DEFAULT_BLOCK_NAME = 'default_block'
16
+
17
+ # @param [Document] document
18
+ #
19
+ # @return [String]
20
+ #
21
+ def self.document_to_lambda_string(document)
22
+ generator = self.new
23
+ generator.generate_lambda_string(document)
24
+ end
25
+
26
+ # @param [Bade::AST::Document] document
27
+ #
28
+ # @return [String] string to parse with Ruby
29
+ #
30
+ def generate_lambda_string(document)
31
+ @document = document
32
+ @buff = []
33
+ @indent = 0
34
+ @code_indent = 0
35
+
36
+ buff_code '# frozen_string_literal: true' # so it can be faster on Ruby 2.3+
37
+ buff_code ''
38
+ buff_code "lambda do |#{NEW_LINE_NAME}: \"\\n\", #{BASE_INDENT_NAME}: ' '|"
39
+
40
+ code_indent {
41
+ buff_code "self.#{NEW_LINE_NAME} = #{NEW_LINE_NAME}"
42
+ buff_code "self.#{BASE_INDENT_NAME} = #{BASE_INDENT_NAME}"
43
+
44
+ visit_document(document)
45
+
46
+ buff_code "output = #{BUFF_NAME}.join"
47
+ buff_code 'self.__reset'
48
+ buff_code 'output'
49
+ }
50
+
51
+ buff_code 'end'
52
+
53
+
54
+ @document = nil
55
+
56
+ @buff.join("\n")
57
+ end
58
+
59
+ # @param [String] text
60
+ #
61
+ def buff_print_text(text, indent: false, new_line: false)
62
+ buff_print_value(%Q{%Q{#{text}}}) if text.length > 0
63
+ end
64
+
65
+ def buff_print_value(value)
66
+ # buff_code %Q{#{BUFF_NAME} << #{CURRENT_INDENT_NAME}} if indent
67
+ buff_code(%Q{#{BUFF_NAME} << #{value}})
68
+ end
69
+
70
+ def buff_code(text)
71
+ @buff << ' ' * @code_indent + text
72
+ end
73
+
74
+
75
+ # @param document [Bade::Document]
76
+ #
77
+ def visit_document(document)
78
+ document.sub_documents.each do |sub_document|
79
+ visit_document(sub_document)
80
+ end
81
+
82
+ buff_code("# ----- start file #{document.file_path}") unless document.file_path.nil?
83
+ visit_node(document.root)
84
+ buff_code("# ----- end file #{document.file_path}") unless document.file_path.nil?
85
+ end
86
+
87
+ # @param current_node [Node]
88
+ #
89
+ def visit_node_children(current_node)
90
+ visit_nodes(current_node.children)
91
+ end
92
+
93
+ # @param nodes [Array<Node>]
94
+ #
95
+ def visit_nodes(nodes)
96
+ nodes.each { |node|
97
+ visit_node(node)
98
+ }
99
+ end
100
+
101
+ # @param current_node [Node]
102
+ #
103
+ def visit_node(current_node)
104
+ case current_node.type
105
+ when :root
106
+ visit_node_children(current_node)
107
+
108
+ when :text
109
+ buff_print_text current_node.value
110
+
111
+ when :tag
112
+ visit_tag(current_node)
113
+
114
+ when :code
115
+ buff_code(current_node.value)
116
+
117
+ when :html_comment
118
+ buff_print_text '<!-- '
119
+ visit_node_children(current_node)
120
+ buff_print_text ' -->'
121
+
122
+ when :comment
123
+ comment_text = '#' + current_node.children.map(&:value).join("\n#")
124
+ buff_code(comment_text)
125
+
126
+ when :doctype
127
+ buff_print_text current_node.xml_output
128
+
129
+ when :mixin_decl
130
+ params = formatted_mixin_params(current_node)
131
+ buff_code "#{MIXINS_NAME}['#{current_node.name}'] = __create_mixin('#{current_node.name}', &lambda { |#{params}|"
132
+
133
+ code_indent {
134
+ blocks_name_declaration(current_node)
135
+ visit_nodes(current_node.children - current_node.params)
136
+ }
137
+
138
+ buff_code '})'
139
+
140
+ when :mixin_call
141
+ params = formatted_mixin_params(current_node)
142
+ buff_code "#{MIXINS_NAME}['#{current_node.name}'].call!(#{params})"
143
+
144
+ when :output
145
+ data = current_node.value
146
+ output_code = if current_node.escaped
147
+ "\#{__html_escaped(#{data})}"
148
+ else
149
+ "\#{#{data}}"
150
+ end
151
+ buff_print_text output_code
152
+
153
+ when :newline
154
+ # buff_print_value(NEW_LINE_NAME)
155
+
156
+ when :import
157
+ base_path = File.expand_path(current_node.value, File.dirname(@document.file_path))
158
+ load_path = if base_path.end_with?('.rb') && File.exist?(base_path)
159
+ base_path
160
+ elsif File.exist?("#{base_path}.rb")
161
+ "#{base_path}.rb"
162
+ else
163
+ nil # other cases are handled in Renderer
164
+ end
165
+
166
+ buff_code "load('#{load_path}')" unless load_path.nil?
167
+
168
+ else
169
+ raise "Unknown type #{current_node.type}"
170
+ end
171
+ end
172
+
173
+ # @param [TagNode] current_node
174
+ #
175
+ # @return [nil]
176
+ #
177
+ def visit_tag(current_node)
178
+ attributes = formatted_attributes(current_node)
179
+ children_wo_attributes = (current_node.children - current_node.attributes)
180
+
181
+ text = "<#{current_node.name}"
7
182
 
8
- # @param [Node] root
183
+ if attributes.length > 0
184
+ text += "#{attributes}"
185
+ end
186
+
187
+ other_than_new_lines = children_wo_attributes.any? { |node|
188
+ node.type != :newline
189
+ }
190
+
191
+ if other_than_new_lines
192
+ text += '>'
193
+ else
194
+ text += '/>'
195
+ end
196
+
197
+ conditional_nodes = current_node.children.select { |n| n.type == :output && n.conditional }
198
+
199
+ unless conditional_nodes.empty?
200
+ buff_code "if (#{conditional_nodes.map(&:value).join(') && (')})"
201
+
202
+ @code_indent += 1
203
+ end
204
+
205
+ buff_print_text(text, new_line: true, indent: true)
206
+
207
+ if other_than_new_lines
208
+ last_node = children_wo_attributes.last
209
+ is_last_newline = !last_node.nil? && last_node.type == :newline
210
+ nodes = if is_last_newline
211
+ children_wo_attributes[0...-1]
212
+ else
213
+ children_wo_attributes
214
+ end
215
+
216
+ code_indent do
217
+ visit_nodes(nodes)
218
+ end
219
+
220
+ buff_print_text("</#{current_node.name}>", new_line: true, indent: true)
221
+
222
+ # print new line after the tag
223
+ visit_node(last_node) if is_last_newline
224
+ end
225
+
226
+ unless conditional_nodes.empty?
227
+ @code_indent -= 1
228
+
229
+ buff_code 'end'
230
+ end
231
+ end
232
+
233
+ # @param [TagNode] tag_node
9
234
  #
10
- # @return [Lambda]
235
+ # @return [String] formatted attributes
11
236
  #
12
- def self.node_to_lambda(root, new_line: "\n", indent: "\t", filename: '')
237
+ def formatted_attributes(tag_node)
238
+ all_attributes = Hash.new { |hash, key| hash[key] = [] }
239
+ xml_attributes = []
240
+
241
+ tag_node.attributes.each do |attr|
242
+ unless all_attributes.include?(attr.name)
243
+ xml_attributes << attr.name
244
+ end
13
245
 
246
+ all_attributes[attr.name] << attr.value
247
+ end
248
+
249
+ xml_attributes.map do |attr_name|
250
+ joined = all_attributes[attr_name].join('), (')
251
+ %Q{\#{__tag_render_attribute('#{attr_name}', (#{joined}))}}
252
+ end.join
253
+ end
254
+
255
+ # Method for indenting generated code, indent is raised only in passed block
256
+ #
257
+ # @return [nil]
258
+ #
259
+ def code_indent(plus = 1)
260
+ @code_indent += plus
261
+ yield
262
+ @code_indent -= plus
263
+ end
264
+
265
+ # @param [MixinCommonNode] mixin_node
266
+ #
267
+ # @return [String] formatted params
268
+ #
269
+ def formatted_mixin_params(mixin_node)
270
+ params = mixin_node.params
271
+ result = []
272
+
273
+ if mixin_node.type == :mixin_call
274
+ blocks = mixin_node.blocks
275
+
276
+ other_children = (mixin_node.children - mixin_node.blocks - mixin_node.params)
277
+ if other_children.reject { |n| n.type == :newline }.count > 0
278
+ def_block_node = AST::NodeRegistrator.create(:mixin_block, mixin_node.lineno)
279
+ def_block_node.name = DEFAULT_BLOCK_NAME
280
+ def_block_node.children = other_children
281
+
282
+ blocks << def_block_node
283
+ end
284
+
285
+ if blocks.length > 0
286
+ buff_code '__blocks = {}'
287
+
288
+ blocks.each do |block|
289
+ block_definition(block)
290
+ end
291
+
292
+ result << '__blocks.dup'
293
+ else
294
+ result << '{}'
295
+ end
296
+ elsif mixin_node.type == :mixin_decl
297
+ result << '__blocks'
298
+ end
299
+
300
+
301
+ # normal params
302
+ result += params.select { |n| n.type == :mixin_param }.map(&:value)
303
+ result += params.select { |n| n.type == :mixin_key_param }.map { |param| "#{param.name}: #{param.value}" }
304
+
305
+ result.join(', ')
306
+ end
307
+
308
+ # Generates code for definition of block
309
+ #
310
+ # @param [MixinCallBlockNode] block_node
311
+ #
312
+ # @return [nil]
313
+ #
314
+ def block_definition(block_node)
315
+ buff_code "__blocks['#{block_node.name}'] = __create_block('#{block_node.name}') do"
316
+
317
+ code_indent do
318
+ buff_code '__buffs_push()'
319
+
320
+ visit_node_children(block_node)
321
+
322
+ buff_code '__buffs_pop()'
323
+ end
324
+
325
+ buff_code 'end'
326
+ end
327
+
328
+ # Generates code for block variables declaration in mixin definition
329
+ #
330
+ # @param [String] block_name
331
+ #
332
+ # @return [nil]
333
+ #
334
+ def block_name_declaration(block_name)
335
+ buff_code "#{block_name} = __blocks.delete('#{block_name}') { __create_block('#{block_name}') }"
336
+ end
337
+
338
+ # @param [MixinDeclarationNode] mixin_node
339
+ #
340
+ # @return [nil]
341
+ #
342
+ def blocks_name_declaration(mixin_node)
343
+ block_name_declaration(DEFAULT_BLOCK_NAME)
344
+
345
+ mixin_node.params.select { |n| n.type == :mixin_block_param }.each do |param|
346
+ block_name_declaration(param.value)
347
+ end
348
+ end
349
+
350
+
351
+
352
+ # @param [String] str
353
+ #
354
+ # @return [Void]
355
+ #
356
+ def escape_double_quotes!(str)
357
+ str.gsub!(/"/, '\"')
14
358
  end
15
359
  end
360
+
361
+ # backward compatibility
362
+ RubyGenerator = Generator
16
363
  end
data/lib/bade/parser.rb CHANGED
@@ -1,7 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'ast/node'
4
+ require_relative 'ast/document'
5
+ require_relative 'ast/node_registrator'
1
6
 
2
- require_relative 'node'
3
- require_relative 'document'
4
7
  require_relative 'ruby_extensions/string'
8
+ require_relative 'ruby_extensions/array'
5
9
 
6
10
  module Bade
7
11
  class Parser
@@ -29,35 +33,36 @@ module Bade
29
33
 
30
34
  class ParserInternalError < StandardError; end
31
35
 
36
+ # @type @stacks [Array<Bade::Node>]
37
+
32
38
  # @return [Array<String>]
33
39
  #
34
40
  attr_reader :dependency_paths
35
41
 
36
- # Initialize
42
+ # @return [String]
37
43
  #
38
- # Available options:
39
- # :tabsize [Int] default 4
40
- # :file [String] default nil
44
+ attr_reader :file_path
45
+
46
+ # @param [Fixnum] tabsize
47
+ # @param [String] file_path
41
48
  #
42
- def initialize(options = {})
49
+ def initialize(tabsize: 4, file_path: nil)
43
50
  @line = ''
44
51
 
45
- tabsize = options.delete(:tabsize) { 4 }
46
52
  @tabsize = tabsize
53
+ @file_path = file_path
47
54
 
48
55
  @tab_re = /\G((?: {#{tabsize}})*) {0,#{tabsize-1}}\t/
49
56
  @tab = '\1' + ' ' * tabsize
50
57
 
51
- @options = options
52
-
53
58
  reset
54
59
  end
55
60
 
56
61
  # @param [String, Array<String>] str
57
- # @return [Bade::Document] root node
62
+ # @return [Bade::AST::Document] root node
58
63
  #
59
64
  def parse(str)
60
- @document = Document.new(file_path: @options[:file_path])
65
+ @document = AST::Document.new(file_path: file_path)
61
66
  @root = @document.root
62
67
 
63
68
  @dependency_paths = []
@@ -65,7 +70,7 @@ module Bade
65
70
  if str.kind_of? Array
66
71
  reset(str, [[@root]])
67
72
  else
68
- reset(str.split(/\r?\n/), [[@root]])
73
+ reset(str.split(/\r?\n/, -1), [[@root]]) # -1 is for not suppressing empty lines
69
74
  end
70
75
 
71
76
  parse_line while next_line
@@ -81,7 +86,7 @@ module Bade
81
86
  NAME_RE_STRING = "(#{WORD_RE}(?:#{WORD_RE}|:|-|_)*)"
82
87
 
83
88
  ATTR_NAME_RE_STRING = "\\A\\s*#{NAME_RE_STRING}"
84
- CODE_ATTR_RE = /#{ATTR_NAME_RE_STRING}\s*(&?):\s*/
89
+ CODE_ATTR_RE = /#{ATTR_NAME_RE_STRING}\s*&?:\s*/
85
90
 
86
91
  TAG_RE = /\A#{NAME_RE_STRING}/
87
92
  CLASS_TAG_RE = /\A\.#{NAME_RE_STRING}/
@@ -119,6 +124,11 @@ module Bade
119
124
  def next_line
120
125
  if @lines.empty?
121
126
  @orig_line = @line = nil
127
+
128
+ last_newlines = remove_last_newlines
129
+ @root.children += last_newlines
130
+
131
+ nil
122
132
  else
123
133
  @orig_line = @lines.shift
124
134
  @lineno += 1
@@ -140,16 +150,16 @@ module Bade
140
150
  #
141
151
  # @param [Symbol] type
142
152
  #
143
- def append_node(type, indent: @indents.length, add: false, data: nil)
153
+ def append_node(type, indent: @indents.length, add: false, value: nil)
144
154
  while indent >= @stacks.length
145
155
  @stacks << @stacks.last.dup
146
156
  end
147
157
 
148
158
  parent = @stacks[indent].last
149
- node = Node.create(type, parent)
150
- node.lineno = @lineno
159
+ node = AST::NodeRegistrator.create(type, @lineno)
160
+ parent.children << node
151
161
 
152
- node.data = data
162
+ node.value = value unless value.nil?
153
163
 
154
164
  if add
155
165
  @stacks[indent] << node
@@ -158,19 +168,24 @@ module Bade
158
168
  node
159
169
  end
160
170
 
161
- def parse_line
162
- line = @line
171
+ # @return [Array<AST::Node>]
172
+ #
173
+ def remove_last_newlines
174
+ last_node = @stacks.last.last
175
+ last_newlines_count = last_node.children.rcount_matching { |n| n.type == :newline }
176
+ last_node.children.pop(last_newlines_count)
177
+ end
163
178
 
164
- if line =~ /\A\s*\Z/
165
- append_node :newline
179
+ def parse_line
180
+ if @line.strip.length == 0
181
+ append_node(:newline) unless @lines.empty?
166
182
  return
167
183
  end
168
184
 
169
- indent = get_indent(line)
185
+ indent = get_indent(@line)
170
186
 
171
187
  # left strip
172
- line.remove_indent!(indent, @tabsize)
173
- @line = line
188
+ @line.remove_indent!(indent, @tabsize)
174
189
 
175
190
  # If there's more stacks than indents, it means that the previous
176
191
  # line is expecting this line to be indented.
@@ -181,19 +196,36 @@ module Bade
181
196
  else
182
197
  # This line was *not* indented more than the line before,
183
198
  # so we'll just forget about the stack that the previous line pushed.
184
- @stacks.pop if expecting_indentation
199
+ if expecting_indentation
200
+ last_newlines = remove_last_newlines
201
+
202
+ @stacks.pop
203
+
204
+ new_node = @stacks.last.last
205
+ new_node.children += last_newlines
206
+ end
185
207
 
186
208
  # This line was deindented.
187
209
  # Now we're have to go through the all the indents and figure out
188
210
  # how many levels we've deindented.
189
211
  while indent < @indents.last
212
+ last_newlines = remove_last_newlines
213
+
190
214
  @indents.pop
191
215
  @stacks.pop
216
+
217
+ new_node = @stacks.last.last
218
+ new_node.children += last_newlines
192
219
  end
193
220
 
194
221
  # Remove old stacks we don't need
195
222
  while not @stacks[indent].nil? and indent < @stacks[indent].length - 1
223
+ last_newlines = remove_last_newlines
224
+
196
225
  @stacks[indent].pop
226
+
227
+ new_node = @stacks.last.last
228
+ new_node.children += last_newlines
197
229
  end
198
230
 
199
231
  # This line's indentation happens lie "between" two other line's
@@ -208,83 +240,98 @@ module Bade
208
240
  parse_line_indicators
209
241
  end
210
242
 
211
- def parse_line_indicators
212
- add_new_line = true
243
+ module LineIndicatorRegexps
244
+ IMPORT = /\Aimport /
245
+ MIXIN_DECL = /\Amixin #{NAME_RE_STRING}/
246
+ MIXIN_CALL = /\A\+#{NAME_RE_STRING}/
247
+ BLOCK_DECLARATION = /\Ablock #{NAME_RE_STRING}/
248
+ HTML_COMMENT = /\A\/\/! /
249
+ NORMAL_COMMENT = /\A\/\//
250
+ TEXT_BLOCK_START = /\A\|( ?)/
251
+ INLINE_HTML = /\A</
252
+ CODE_BLOCK = /\A-/
253
+ OUTPUT_BLOCK = /\A(\??)(&?)=/
254
+ DOCTYPE = /\Adoctype\s/i
255
+ TAG_CLASS_START_BLOCK = /\A\./
256
+ TAG_ID_START_BLOCK = /\A#/
257
+ end
258
+
259
+ module ParseRubyCodeRegexps
260
+ END_NEW_LINE = /\A\s*\n/
261
+ END_PARAMS_ARG = /\A\s*[,)]/
262
+ end
213
263
 
264
+ def parse_line_indicators(add_newline: true)
214
265
  case @line
215
- when /\Aimport /
266
+ when LineIndicatorRegexps::IMPORT
216
267
  @line = $'
217
268
  parse_import
218
269
 
219
- when /\Amixin #{NAME_RE_STRING}/
270
+ when LineIndicatorRegexps::MIXIN_DECL
220
271
  # Mixin declaration
221
272
  @line = $'
222
273
  parse_mixin_declaration($1)
223
274
 
224
- when /\A\+#{NAME_RE_STRING}/
275
+ when LineIndicatorRegexps::MIXIN_CALL
225
276
  # Mixin call
226
277
  @line = $'
227
278
  parse_mixin_call($1)
228
279
 
229
- when /\Ablock #{NAME_RE_STRING}/
280
+ when LineIndicatorRegexps::BLOCK_DECLARATION
230
281
  @line = $'
231
282
  if @stacks.last.last.type == :mixin_call
232
- append_node :mixin_block, data: $1, add: true
283
+ node = append_node(:mixin_block, add: true)
284
+ node.name = $1
233
285
  else
234
286
  # keyword block used outside of mixin call
235
287
  parse_tag($&)
236
288
  end
237
289
 
238
- when /\A\/\/! /
290
+ when LineIndicatorRegexps::HTML_COMMENT
239
291
  # HTML comment
240
- append_node :html_comment, add: true
292
+ append_node(:html_comment, add: true)
241
293
  parse_text_block $', @indents.last + @tabsize
242
294
 
243
- when /\A\/\//
295
+ when LineIndicatorRegexps::NORMAL_COMMENT
244
296
  # Comment
245
- append_node :comment, add: true
297
+ append_node(:comment, add: true)
246
298
  parse_text_block $', @indents.last + @tabsize
247
299
 
248
- when /\A\|( ?)/
300
+ when LineIndicatorRegexps::TEXT_BLOCK_START
249
301
  # Found a text block.
250
302
  parse_text_block $', @indents.last + @tabsize
251
303
 
252
- when /\A</
304
+ when LineIndicatorRegexps::INLINE_HTML
253
305
  # Inline html
254
- append_node :text, data: @line
306
+ append_node(:text, value: @line)
255
307
 
256
- when /\A-\s*(.*)\Z/
308
+ when LineIndicatorRegexps::CODE_BLOCK
257
309
  # Found a code block.
258
- code_node = append_node :ruby_code
259
- code_node.data = $1
260
- add_new_line = false
310
+ append_node(:code, value: $'.strip)
261
311
 
262
- when /\A(&?)=/
312
+ when LineIndicatorRegexps::OUTPUT_BLOCK
263
313
  # Found an output block.
264
314
  # We expect the line to be broken or the next line to be indented.
265
315
  @line = $'
266
- output_node = append_node :output
267
- output_node.escaped = $1.length == 1
268
- output_node.data = parse_ruby_code("\n")
269
-
270
- when /\A(\w+):\s*\Z/
271
- # Embedded template detected. It is treated as block.
272
- @stacks.last << [:slim, :embedded, $1, parse_text_block]
316
+ output_node = append_node(:output)
317
+ output_node.conditional = $1.length == 1
318
+ output_node.escaped = $2.length == 1
319
+ output_node.value = parse_ruby_code(ParseRubyCodeRegexps::END_NEW_LINE)
273
320
 
274
- when /\Adoctype\s/i
321
+ when LineIndicatorRegexps::DOCTYPE
275
322
  # Found doctype declaration
276
- append_node :doctype, data: $'.strip
323
+ append_node(:doctype, value: $'.strip)
277
324
 
278
325
  when TAG_RE
279
326
  # Found a HTML tag.
280
327
  @line = $' if $1
281
328
  parse_tag($&)
282
329
 
283
- when /\A\./
330
+ when LineIndicatorRegexps::TAG_CLASS_START_BLOCK
284
331
  # Found class name -> implicit div
285
332
  parse_tag 'div'
286
333
 
287
- when /\A#/
334
+ when LineIndicatorRegexps::TAG_ID_START_BLOCK
288
335
  # Found id name -> implicit div
289
336
  parse_tag 'div'
290
337
 
@@ -292,38 +339,54 @@ module Bade
292
339
  syntax_error 'Unknown line indicator'
293
340
  end
294
341
 
295
- append_node :newline if add_new_line
342
+ append_node(:newline) if add_newline && !@lines.empty?
296
343
  end
297
344
 
298
345
  def parse_import
299
346
  path = eval(@line)
300
- import_node = append_node :import
301
- import_node.data = path
347
+ append_node(:import, value: path)
302
348
 
303
349
  @dependency_paths << path unless @dependency_paths.include?(path)
304
350
  end
305
351
 
352
+ module MixinRegexps
353
+ TEXT_START = /\A /
354
+ BLOCK_EXPANSION = /\A:\s+/
355
+ OUTPUT_CODE = /\A(&?)=/
356
+
357
+ PARAMS_END = /\A\s*\)/
358
+
359
+ PARAMS_END_SPACES = /^\s*$/
360
+ PARAMS_ARGS_DELIMITER = /\A\s*,/
361
+
362
+ PARAMS_PARAM_NAME = /\A\s*#{NAME_RE_STRING}/
363
+ PARAMS_BLOCK_NAME = /\A\s*&#{NAME_RE_STRING}/
364
+ PARAMS_KEY_PARAM_NAME = CODE_ATTR_RE
365
+ end
366
+
306
367
  def parse_mixin_call(mixin_name)
307
- mixin_node = append_node :mixin_call, add: true
308
- mixin_node.data = mixin_name
368
+ mixin_name = fixed_trailing_colon(mixin_name)
369
+
370
+ mixin_node = append_node(:mixin_call, add: true)
371
+ mixin_node.name = mixin_name
309
372
 
310
373
  parse_mixin_call_params
311
374
 
312
375
  case @line
313
- when /\A /
376
+ when MixinRegexps::TEXT_START
314
377
  @line = $'
315
378
  parse_text
316
379
 
317
- when /\A:\s+/
380
+ when MixinRegexps::BLOCK_EXPANSION
318
381
  # Block expansion
319
382
  @line = $'
320
- parse_line_indicators
383
+ parse_line_indicators(add_newline: false)
321
384
 
322
- when /\A(&?)=/
385
+ when MixinRegexps::OUTPUT_CODE
323
386
  # Handle output code
324
- parse_line_indicators
387
+ parse_line_indicators(add_newline: false)
325
388
 
326
- when /^$/
389
+ when ''
327
390
  # nothing
328
391
 
329
392
  else
@@ -334,47 +397,45 @@ module Bade
334
397
  def parse_mixin_call_params
335
398
  # between tag name and attribute must not be space
336
399
  # and skip when is nothing other
337
- if @line =~ /\A\(/
338
- @line = $'
400
+ if @line.start_with?('(')
401
+ @line.remove_first!
339
402
  else
340
403
  return
341
404
  end
342
405
 
343
- end_re = /\A\s*\)/
344
-
345
406
  while true
346
407
  case @line
347
- when CODE_ATTR_RE
408
+ when MixinRegexps::PARAMS_KEY_PARAM_NAME
348
409
  @line = $'
349
- attr_node = append_node :mixin_key_param
350
- attr_node.name = $1
351
- attr_node.value = parse_ruby_code(',)')
410
+ attr_node = append_node(:mixin_key_param)
411
+ attr_node.name = fixed_trailing_colon($1)
412
+ attr_node.value = parse_ruby_code(ParseRubyCodeRegexps::END_PARAMS_ARG)
352
413
 
353
- when /\A\s*,/
414
+ when MixinRegexps::PARAMS_ARGS_DELIMITER
354
415
  # args delimiter
355
416
  @line = $'
356
417
  next
357
418
 
358
- when /^\s*$/
419
+ when MixinRegexps::PARAMS_END_SPACES
359
420
  # spaces and/or end of line
360
421
  next_line
361
422
  next
362
423
 
363
- when end_re
424
+ when MixinRegexps::PARAMS_END
364
425
  # Find ending delimiter
365
426
  @line = $'
366
427
  break
367
428
 
368
429
  else
369
- attr_node = append_node :mixin_param
370
- attr_node.data = parse_ruby_code(',)')
430
+ attr_node = append_node(:mixin_param)
431
+ attr_node.value = parse_ruby_code(ParseRubyCodeRegexps::END_PARAMS_ARG)
371
432
  end
372
433
  end
373
434
  end
374
435
 
375
436
  def parse_mixin_declaration(mixin_name)
376
- mixin_node = append_node :mixin_declaration, add: true
377
- mixin_node.data = mixin_name
437
+ mixin_node = append_node(:mixin_decl, add: true)
438
+ mixin_node.name = mixin_name
378
439
 
379
440
  parse_mixin_declaration_params
380
441
  end
@@ -382,37 +443,35 @@ module Bade
382
443
  def parse_mixin_declaration_params
383
444
  # between tag name and attribute must not be space
384
445
  # and skip when is nothing other
385
- if @line =~ /\A\(/
386
- @line = $'
446
+ if @line.start_with?('(')
447
+ @line.remove_first!
387
448
  else
388
449
  return
389
450
  end
390
451
 
391
- end_re = /\A\s*\)/
392
-
393
452
  while true
394
453
  case @line
395
- when CODE_ATTR_RE
454
+ when MixinRegexps::PARAMS_KEY_PARAM_NAME
396
455
  # Value ruby code
397
456
  @line = $'
398
- attr_node = append_node :mixin_key_param
399
- attr_node.name = $1
400
- attr_node.value = parse_ruby_code(',)')
457
+ attr_node = append_node(:mixin_key_param)
458
+ attr_node.name = fixed_trailing_colon($1)
459
+ attr_node.value = parse_ruby_code(ParseRubyCodeRegexps::END_PARAMS_ARG)
401
460
 
402
- when /\A\s*#{NAME_RE_STRING}/
461
+ when MixinRegexps::PARAMS_PARAM_NAME
403
462
  @line = $'
404
- append_node :mixin_param, data: $1
463
+ append_node(:mixin_param, value: $1)
405
464
 
406
- when /\A\s*&#{NAME_RE_STRING}/
465
+ when MixinRegexps::PARAMS_BLOCK_NAME
407
466
  @line = $'
408
- append_node :mixin_block_param, data: $1
467
+ append_node(:mixin_block_param, value: $1)
409
468
 
410
- when /\A\s*,/
469
+ when MixinRegexps::PARAMS_ARGS_DELIMITER
411
470
  # args delimiter
412
471
  @line = $'
413
472
  next
414
473
 
415
- when end_re
474
+ when MixinRegexps::PARAMS_END
416
475
  # Find ending delimiter
417
476
  @line = $'
418
477
  break
@@ -425,17 +484,26 @@ module Bade
425
484
 
426
485
  def parse_text
427
486
  text = @line
428
- text = text.gsub(/&\{/, '#{ html_escaped ')
429
- append_node :text, data: text
487
+ text = text.gsub(/&\{/, '#{ __html_escaped ')
488
+ append_node(:text, value: text)
430
489
  end
431
490
 
432
491
 
492
+ module TagRegexps
493
+ BLOCK_EXPANSION = /\A:\s+/
494
+ OUTPUT_CODE = LineIndicatorRegexps::OUTPUT_BLOCK
495
+ TEXT_START = /\A /
496
+
497
+ PARAMS_ARGS_DELIMITER = /\A\s*,/
498
+ PARAMS_END = /\A\s*\)/
499
+ end
500
+
433
501
  # @param value [String]
434
502
  #
435
503
  def fixed_trailing_colon(value)
436
- if value =~ /(:)\Z/
437
- value = value.sub /:\Z/, ''
438
- @line.prepend ':'
504
+ if String === value && value.end_with?(':')
505
+ value = value.remove_last
506
+ @line.prepend(':')
439
507
  end
440
508
 
441
509
  value
@@ -447,30 +515,30 @@ module Bade
447
515
  def parse_tag(tag)
448
516
  tag = fixed_trailing_colon(tag)
449
517
 
450
- if tag.is_a? Node
518
+ if tag.is_a?(AST::Node)
451
519
  tag_node = tag
452
520
  else
453
- tag_node = append_node :tag, add: true
521
+ tag_node = append_node(:tag, add: true)
454
522
  tag_node.name = tag
455
523
  end
456
524
 
457
525
  parse_tag_attributes
458
526
 
459
527
  case @line
460
- when /\A:\s+/
528
+ when TagRegexps::BLOCK_EXPANSION
461
529
  # Block expansion
462
530
  @line = $'
463
- parse_line_indicators
531
+ parse_line_indicators(add_newline: false)
464
532
 
465
- when /\A(&?)=/
533
+ when TagRegexps::OUTPUT_CODE
466
534
  # Handle output code
467
- parse_line_indicators
535
+ parse_line_indicators(add_newline: false)
468
536
 
469
537
  when CLASS_TAG_RE
470
538
  # Class name
471
539
  @line = $'
472
540
 
473
- attr_node = append_node :tag_attribute
541
+ attr_node = append_node(:tag_attr)
474
542
  attr_node.name = 'class'
475
543
  attr_node.value = fixed_trailing_colon($1).single_quote
476
544
 
@@ -480,18 +548,18 @@ module Bade
480
548
  # Id name
481
549
  @line = $'
482
550
 
483
- attr_node = append_node :tag_attribute
551
+ attr_node = append_node(:tag_attr)
484
552
  attr_node.name = 'id'
485
553
  attr_node.value = fixed_trailing_colon($1).single_quote
486
554
 
487
555
  parse_tag tag_node
488
556
 
489
- when /\A /
557
+ when TagRegexps::TEXT_START
490
558
  # Text content
491
559
  @line = $'
492
560
  parse_text
493
561
 
494
- when /^$/
562
+ when ''
495
563
  # nothing
496
564
 
497
565
  else
@@ -504,29 +572,27 @@ module Bade
504
572
 
505
573
  # between tag name and attribute must not be space
506
574
  # and skip when is nothing other
507
- if @line =~ /\A\(/
508
- @line = $'
575
+ if @line.start_with?('(')
576
+ @line.remove_first!
509
577
  else
510
578
  return
511
579
  end
512
580
 
513
- end_re = /\A\s*\)/
514
-
515
581
  while true
516
582
  case @line
517
583
  when CODE_ATTR_RE
518
584
  # Value ruby code
519
585
  @line = $'
520
- attr_node = append_node :tag_attribute
586
+ attr_node = append_node(:tag_attr)
521
587
  attr_node.name = $1
522
- attr_node.value = parse_ruby_code(',)')
588
+ attr_node.value = parse_ruby_code(ParseRubyCodeRegexps::END_PARAMS_ARG)
523
589
 
524
- when /\A\s*,/
590
+ when TagRegexps::PARAMS_ARGS_DELIMITER
525
591
  # args delimiter
526
592
  @line = $'
527
593
  next
528
594
 
529
- when end_re
595
+ when TagRegexps::PARAMS_END
530
596
  # Find ending delimiter
531
597
  @line = $'
532
598
  break
@@ -537,8 +603,8 @@ module Bade
537
603
  syntax_error('Expected attribute') unless @line.empty?
538
604
 
539
605
  # Attributes span multiple lines
540
- @stacks.last << [:newline]
541
- syntax_error("Expected closing delimiter #{delimiter}") if @lines.empty?
606
+ append_node(:newline)
607
+ syntax_error('Expected closing tag attributes delimiter `)`') if @lines.empty?
542
608
  next_line
543
609
  end
544
610
  end
@@ -553,9 +619,9 @@ module Bade
553
619
  end
554
620
 
555
621
  until @lines.empty?
556
- if @lines.first =~ /\A\s*\Z/
622
+ if @lines.first.blank?
557
623
  next_line
558
- append_node :newline
624
+ append_node(:newline)
559
625
  else
560
626
  indent = get_indent(@lines.first)
561
627
  break if indent <= @indents.last
@@ -575,13 +641,17 @@ module Bade
575
641
 
576
642
  # Parse ruby code, ended with outer delimiters
577
643
  #
578
- # @param [String] outer_delimiters
644
+ # @param [String, Regexp] outer_delimiters
579
645
  #
580
646
  # @return [Void] parsed ruby code
581
647
  #
582
648
  def parse_ruby_code(outer_delimiters)
583
- code = ''
584
- end_re = /\A\s*[#{Regexp.escape outer_delimiters.to_s}]/
649
+ code = String.new
650
+ end_re = if Regexp === outer_delimiters
651
+ outer_delimiters
652
+ else
653
+ /\A\s*[#{Regexp.escape outer_delimiters.to_s}]/
654
+ end
585
655
  delimiters = []
586
656
 
587
657
  until @line.empty? or (delimiters.count == 0 and @line =~ end_re)
@@ -634,8 +704,8 @@ module Bade
634
704
  RUBY_END_DELIMITERS = (%w(\) ] }) + RUBY_NOT_NESTABLE_DELIMITERS).freeze
635
705
  RUBY_ALL_DELIMITERS = (RUBY_START_DELIMITERS + RUBY_END_DELIMITERS).uniq.freeze
636
706
 
637
- RUBY_START_DELIMITERS_RE = /\A[#{Regexp.escape RUBY_START_DELIMITERS.join('')}]/
638
- RUBY_END_DELIMITERS_RE = /\A[#{Regexp.escape RUBY_END_DELIMITERS.join('')}]/
707
+ RUBY_START_DELIMITERS_RE = /\A[#{Regexp.escape RUBY_START_DELIMITERS.join}]/
708
+ RUBY_END_DELIMITERS_RE = /\A[#{Regexp.escape RUBY_END_DELIMITERS.join}]/
639
709
 
640
710
 
641
711
  # ----------- Errors ---------------
@@ -645,7 +715,7 @@ module Bade
645
715
  # @param [String] message
646
716
  #
647
717
  def syntax_error(message)
648
- raise SyntaxError.new(message, @options[:file], @orig_line, @lineno,
718
+ raise SyntaxError.new(message, file_path, @orig_line, @lineno,
649
719
  @orig_line && @line ? @orig_line.size - @line.size : 0)
650
720
  end
651
721
  end