bade 0.1.4 → 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.
@@ -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