bade 0.2.0 → 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Bade.gemspec +16 -15
- data/lib/bade/ast/document.rb +1 -1
- data/lib/bade/ast/node/doctype_node.rb +6 -6
- data/lib/bade/ast/node/static_text_node.rb +27 -0
- data/lib/bade/ast/node_registrator.rb +3 -4
- data/lib/bade/ast/string_serializer.rb +4 -3
- data/lib/bade/generator.rb +94 -88
- data/lib/bade/parser.rb +28 -587
- data/lib/bade/parser/parser_constants.rb +18 -0
- data/lib/bade/parser/parser_lines.rb +213 -0
- data/lib/bade/parser/parser_mixin.rb +139 -0
- data/lib/bade/parser/parser_ruby_code.rb +77 -0
- data/lib/bade/parser/parser_tag.rb +116 -0
- data/lib/bade/parser/parser_text.rb +78 -0
- data/lib/bade/precompiled.rb +2 -2
- data/lib/bade/renderer.rb +17 -12
- data/lib/bade/ruby_extensions/array.rb +9 -13
- data/lib/bade/ruby_extensions/string.rb +15 -8
- data/lib/bade/runtime/block.rb +6 -10
- data/lib/bade/runtime/mixin.rb +2 -1
- data/lib/bade/runtime/render_binding.rb +5 -5
- data/lib/bade/version.rb +1 -1
- metadata +25 -4
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
|
4
|
+
module Bade
|
5
|
+
require_relative '../parser'
|
6
|
+
|
7
|
+
class Parser
|
8
|
+
WORD_RE = ''.respond_to?(:encoding) ? '\p{Word}' : '\w'
|
9
|
+
NAME_RE_STRING = "(#{WORD_RE}(?:#{WORD_RE}|:|-|_)*)".freeze
|
10
|
+
|
11
|
+
ATTR_NAME_RE_STRING = "\\A\\s*#{NAME_RE_STRING}".freeze
|
12
|
+
CODE_ATTR_RE = /#{ATTR_NAME_RE_STRING}\s*&?:\s*/
|
13
|
+
|
14
|
+
TAG_RE = /\A#{NAME_RE_STRING}/
|
15
|
+
CLASS_TAG_RE = /\A\.#{NAME_RE_STRING}/
|
16
|
+
ID_TAG_RE = /\A##{NAME_RE_STRING}/
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,213 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
|
4
|
+
module Bade
|
5
|
+
require_relative '../parser'
|
6
|
+
|
7
|
+
class Parser
|
8
|
+
module LineIndicatorRegexps
|
9
|
+
IMPORT = /\Aimport /
|
10
|
+
MIXIN_DECL = /\Amixin #{NAME_RE_STRING}/
|
11
|
+
MIXIN_CALL = /\A\+#{NAME_RE_STRING}/
|
12
|
+
BLOCK_DECLARATION = /\Ablock #{NAME_RE_STRING}/
|
13
|
+
HTML_COMMENT = %r{\A//! }
|
14
|
+
NORMAL_COMMENT = %r{\A//}
|
15
|
+
TEXT_BLOCK_START = /\A\|( ?)/
|
16
|
+
INLINE_HTML = /\A</
|
17
|
+
CODE_BLOCK = /\A-/
|
18
|
+
OUTPUT_BLOCK = /\A(\??)(&?)=/
|
19
|
+
DOCTYPE = /\Adoctype\s/i
|
20
|
+
TAG_CLASS_START_BLOCK = /\A\./
|
21
|
+
TAG_ID_START_BLOCK = /\A#/
|
22
|
+
end
|
23
|
+
|
24
|
+
def reset(lines = nil, stacks = nil)
|
25
|
+
# Since you can indent however you like in Slim, we need to keep a list
|
26
|
+
# of how deeply indented you are. For instance, in a template like this:
|
27
|
+
#
|
28
|
+
# doctype # 0 spaces
|
29
|
+
# html # 0 spaces
|
30
|
+
# head # 1 space
|
31
|
+
# title # 4 spaces
|
32
|
+
#
|
33
|
+
# indents will then contain [0, 1, 4] (when it's processing the last line.)
|
34
|
+
#
|
35
|
+
# We uses this information to figure out how many steps we must "jump"
|
36
|
+
# out when we see an de-indented line.
|
37
|
+
@indents = [0]
|
38
|
+
|
39
|
+
# Whenever we want to output something, we'll *always* output it to the
|
40
|
+
# last stack in this array. So when there's a line that expects
|
41
|
+
# indentation, we simply push a new stack onto this array. When it
|
42
|
+
# processes the next line, the content will then be outputted into that
|
43
|
+
# stack.
|
44
|
+
@stacks = stacks
|
45
|
+
|
46
|
+
@lineno = 0
|
47
|
+
@lines = lines
|
48
|
+
|
49
|
+
# @return [String]
|
50
|
+
@line = @orig_line = nil
|
51
|
+
end
|
52
|
+
|
53
|
+
def next_line
|
54
|
+
if @lines.empty?
|
55
|
+
@orig_line = @line = nil
|
56
|
+
|
57
|
+
last_newlines = remove_last_newlines
|
58
|
+
@root.children += last_newlines
|
59
|
+
|
60
|
+
nil
|
61
|
+
else
|
62
|
+
@orig_line = @lines.shift
|
63
|
+
@lineno += 1
|
64
|
+
@line = @orig_line.dup
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def parse_line
|
69
|
+
if @line.strip.empty?
|
70
|
+
append_node(:newline) unless @lines.empty?
|
71
|
+
return
|
72
|
+
end
|
73
|
+
|
74
|
+
indent = get_indent(@line)
|
75
|
+
|
76
|
+
# left strip
|
77
|
+
@line.remove_indent!(indent, @tabsize)
|
78
|
+
|
79
|
+
# If there's more stacks than indents, it means that the previous
|
80
|
+
# line is expecting this line to be indented.
|
81
|
+
expecting_indentation = @stacks.length > @indents.length
|
82
|
+
|
83
|
+
if indent > @indents.last
|
84
|
+
@indents << indent
|
85
|
+
else
|
86
|
+
# This line was *not* indented more than the line before,
|
87
|
+
# so we'll just forget about the stack that the previous line pushed.
|
88
|
+
if expecting_indentation
|
89
|
+
last_newlines = remove_last_newlines
|
90
|
+
|
91
|
+
@stacks.pop
|
92
|
+
|
93
|
+
new_node = @stacks.last.last
|
94
|
+
new_node.children += last_newlines
|
95
|
+
end
|
96
|
+
|
97
|
+
# This line was deindented.
|
98
|
+
# Now we're have to go through the all the indents and figure out
|
99
|
+
# how many levels we've deindented.
|
100
|
+
while indent < @indents.last
|
101
|
+
last_newlines = remove_last_newlines
|
102
|
+
|
103
|
+
@indents.pop
|
104
|
+
@stacks.pop
|
105
|
+
|
106
|
+
new_node = @stacks.last.last
|
107
|
+
new_node.children += last_newlines
|
108
|
+
end
|
109
|
+
|
110
|
+
# Remove old stacks we don't need
|
111
|
+
while !@stacks[indent].nil? && indent < @stacks[indent].length - 1
|
112
|
+
last_newlines = remove_last_newlines
|
113
|
+
|
114
|
+
@stacks[indent].pop
|
115
|
+
|
116
|
+
new_node = @stacks.last.last
|
117
|
+
new_node.children += last_newlines
|
118
|
+
end
|
119
|
+
|
120
|
+
# This line's indentation happens lie "between" two other line's
|
121
|
+
# indentation:
|
122
|
+
#
|
123
|
+
# hello
|
124
|
+
# world
|
125
|
+
# this # <- This should not be possible!
|
126
|
+
syntax_error('Malformed indentation') if indent != @indents.last
|
127
|
+
end
|
128
|
+
|
129
|
+
parse_line_indicators
|
130
|
+
end
|
131
|
+
|
132
|
+
def parse_line_indicators(add_newline: true)
|
133
|
+
case @line
|
134
|
+
when LineIndicatorRegexps::IMPORT
|
135
|
+
@line = $'
|
136
|
+
parse_import
|
137
|
+
|
138
|
+
when LineIndicatorRegexps::MIXIN_DECL
|
139
|
+
# Mixin declaration
|
140
|
+
@line = $'
|
141
|
+
parse_mixin_declaration($1)
|
142
|
+
|
143
|
+
when LineIndicatorRegexps::MIXIN_CALL
|
144
|
+
# Mixin call
|
145
|
+
@line = $'
|
146
|
+
parse_mixin_call($1)
|
147
|
+
|
148
|
+
when LineIndicatorRegexps::BLOCK_DECLARATION
|
149
|
+
@line = $'
|
150
|
+
if @stacks.last.last.type == :mixin_call
|
151
|
+
node = append_node(:mixin_block, add: true)
|
152
|
+
node.name = $1
|
153
|
+
else
|
154
|
+
# keyword block used outside of mixin call
|
155
|
+
parse_tag($&)
|
156
|
+
end
|
157
|
+
|
158
|
+
when LineIndicatorRegexps::HTML_COMMENT
|
159
|
+
# HTML comment
|
160
|
+
append_node(:html_comment, add: true)
|
161
|
+
parse_text_block $', @indents.last + @tabsize
|
162
|
+
|
163
|
+
when LineIndicatorRegexps::NORMAL_COMMENT
|
164
|
+
# Comment
|
165
|
+
append_node(:comment, add: true)
|
166
|
+
parse_text_block $', @indents.last + @tabsize
|
167
|
+
|
168
|
+
when LineIndicatorRegexps::TEXT_BLOCK_START
|
169
|
+
# Found a text block.
|
170
|
+
parse_text_block $', @indents.last + @tabsize
|
171
|
+
|
172
|
+
when LineIndicatorRegexps::INLINE_HTML
|
173
|
+
# Inline html
|
174
|
+
parse_text
|
175
|
+
|
176
|
+
when LineIndicatorRegexps::CODE_BLOCK
|
177
|
+
# Found a code block.
|
178
|
+
append_node(:code, value: $'.strip)
|
179
|
+
|
180
|
+
when LineIndicatorRegexps::OUTPUT_BLOCK
|
181
|
+
# Found an output block.
|
182
|
+
# We expect the line to be broken or the next line to be indented.
|
183
|
+
@line = $'
|
184
|
+
output_node = append_node(:output)
|
185
|
+
output_node.conditional = $1.length == 1
|
186
|
+
output_node.escaped = $2.length == 1
|
187
|
+
output_node.value = parse_ruby_code(ParseRubyCodeRegexps::END_NEW_LINE)
|
188
|
+
|
189
|
+
when LineIndicatorRegexps::DOCTYPE
|
190
|
+
# Found doctype declaration
|
191
|
+
append_node(:doctype, value: $'.strip)
|
192
|
+
|
193
|
+
when TAG_RE
|
194
|
+
# Found a HTML tag.
|
195
|
+
@line = $' if $1
|
196
|
+
parse_tag($&)
|
197
|
+
|
198
|
+
when LineIndicatorRegexps::TAG_CLASS_START_BLOCK
|
199
|
+
# Found class name -> implicit div
|
200
|
+
parse_tag 'div'
|
201
|
+
|
202
|
+
when LineIndicatorRegexps::TAG_ID_START_BLOCK
|
203
|
+
# Found id name -> implicit div
|
204
|
+
parse_tag 'div'
|
205
|
+
|
206
|
+
else
|
207
|
+
syntax_error 'Unknown line indicator'
|
208
|
+
end
|
209
|
+
|
210
|
+
append_node(:newline) if add_newline && !@lines.empty?
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
@@ -0,0 +1,139 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
|
4
|
+
module Bade
|
5
|
+
require_relative '../parser'
|
6
|
+
|
7
|
+
class Parser
|
8
|
+
module MixinRegexps
|
9
|
+
TEXT_START = /\A /
|
10
|
+
BLOCK_EXPANSION = /\A:\s+/
|
11
|
+
OUTPUT_CODE = /\A(&?)=/
|
12
|
+
|
13
|
+
PARAMS_END = /\A\s*\)/
|
14
|
+
|
15
|
+
PARAMS_END_SPACES = /^\s*$/
|
16
|
+
PARAMS_ARGS_DELIMITER = /\A\s*,/
|
17
|
+
|
18
|
+
PARAMS_PARAM_NAME = /\A\s*#{NAME_RE_STRING}/
|
19
|
+
PARAMS_BLOCK_NAME = /\A\s*&#{NAME_RE_STRING}/
|
20
|
+
PARAMS_KEY_PARAM_NAME = CODE_ATTR_RE
|
21
|
+
end
|
22
|
+
|
23
|
+
def parse_mixin_call(mixin_name)
|
24
|
+
mixin_name = fixed_trailing_colon(mixin_name)
|
25
|
+
|
26
|
+
mixin_node = append_node(:mixin_call, add: true)
|
27
|
+
mixin_node.name = mixin_name
|
28
|
+
|
29
|
+
parse_mixin_call_params
|
30
|
+
|
31
|
+
case @line
|
32
|
+
when MixinRegexps::TEXT_START
|
33
|
+
@line = $'
|
34
|
+
parse_text
|
35
|
+
|
36
|
+
when MixinRegexps::BLOCK_EXPANSION
|
37
|
+
# Block expansion
|
38
|
+
@line = $'
|
39
|
+
parse_line_indicators(add_newline: false)
|
40
|
+
|
41
|
+
when MixinRegexps::OUTPUT_CODE
|
42
|
+
# Handle output code
|
43
|
+
parse_line_indicators(add_newline: false)
|
44
|
+
|
45
|
+
when ''
|
46
|
+
# nothing
|
47
|
+
|
48
|
+
else
|
49
|
+
syntax_error "Unknown symbol after mixin calling, line = `#{@line}'"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def parse_mixin_call_params
|
54
|
+
# between tag name and attribute must not be space
|
55
|
+
# and skip when is nothing other
|
56
|
+
return unless @line.start_with?('(')
|
57
|
+
|
58
|
+
# remove starting bracket
|
59
|
+
@line.remove_first!
|
60
|
+
|
61
|
+
loop do
|
62
|
+
case @line
|
63
|
+
when MixinRegexps::PARAMS_KEY_PARAM_NAME
|
64
|
+
@line = $'
|
65
|
+
attr_node = append_node(:mixin_key_param)
|
66
|
+
attr_node.name = fixed_trailing_colon($1)
|
67
|
+
attr_node.value = parse_ruby_code(ParseRubyCodeRegexps::END_PARAMS_ARG)
|
68
|
+
|
69
|
+
when MixinRegexps::PARAMS_ARGS_DELIMITER
|
70
|
+
# args delimiter
|
71
|
+
@line = $'
|
72
|
+
next
|
73
|
+
|
74
|
+
when MixinRegexps::PARAMS_END_SPACES
|
75
|
+
# spaces and/or end of line
|
76
|
+
next_line
|
77
|
+
next
|
78
|
+
|
79
|
+
when MixinRegexps::PARAMS_END
|
80
|
+
# Find ending delimiter
|
81
|
+
@line = $'
|
82
|
+
break
|
83
|
+
|
84
|
+
else
|
85
|
+
attr_node = append_node(:mixin_param)
|
86
|
+
attr_node.value = parse_ruby_code(ParseRubyCodeRegexps::END_PARAMS_ARG)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def parse_mixin_declaration(mixin_name)
|
92
|
+
mixin_node = append_node(:mixin_decl, add: true)
|
93
|
+
mixin_node.name = mixin_name
|
94
|
+
|
95
|
+
parse_mixin_declaration_params
|
96
|
+
end
|
97
|
+
|
98
|
+
def parse_mixin_declaration_params
|
99
|
+
# between tag name and attribute must not be space
|
100
|
+
# and skip when is nothing other
|
101
|
+
return unless @line.start_with?('(')
|
102
|
+
|
103
|
+
# remove starting bracket
|
104
|
+
@line.remove_first!
|
105
|
+
|
106
|
+
loop do
|
107
|
+
case @line
|
108
|
+
when MixinRegexps::PARAMS_KEY_PARAM_NAME
|
109
|
+
# Value ruby code
|
110
|
+
@line = $'
|
111
|
+
attr_node = append_node(:mixin_key_param)
|
112
|
+
attr_node.name = fixed_trailing_colon($1)
|
113
|
+
attr_node.value = parse_ruby_code(ParseRubyCodeRegexps::END_PARAMS_ARG)
|
114
|
+
|
115
|
+
when MixinRegexps::PARAMS_PARAM_NAME
|
116
|
+
@line = $'
|
117
|
+
append_node(:mixin_param, value: $1)
|
118
|
+
|
119
|
+
when MixinRegexps::PARAMS_BLOCK_NAME
|
120
|
+
@line = $'
|
121
|
+
append_node(:mixin_block_param, value: $1)
|
122
|
+
|
123
|
+
when MixinRegexps::PARAMS_ARGS_DELIMITER
|
124
|
+
# args delimiter
|
125
|
+
@line = $'
|
126
|
+
next
|
127
|
+
|
128
|
+
when MixinRegexps::PARAMS_END
|
129
|
+
# Find ending delimiter
|
130
|
+
@line = $'
|
131
|
+
break
|
132
|
+
|
133
|
+
else
|
134
|
+
syntax_error('wrong mixin attribute syntax')
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
|
4
|
+
module Bade
|
5
|
+
require_relative '../parser'
|
6
|
+
|
7
|
+
class Parser
|
8
|
+
module ParseRubyCodeRegexps
|
9
|
+
END_NEW_LINE = /\A\s*\n/
|
10
|
+
END_PARAMS_ARG = /\A\s*[,)]/
|
11
|
+
end
|
12
|
+
|
13
|
+
# Parse ruby code, ended with outer delimiters
|
14
|
+
#
|
15
|
+
# @param [String, Regexp] outer_delimiters
|
16
|
+
#
|
17
|
+
# @return [Void] parsed ruby code
|
18
|
+
#
|
19
|
+
def parse_ruby_code(outer_delimiters)
|
20
|
+
code = String.new
|
21
|
+
end_re = if outer_delimiters.is_a?(Regexp)
|
22
|
+
outer_delimiters
|
23
|
+
else
|
24
|
+
/\A\s*[#{Regexp.escape outer_delimiters.to_s}]/
|
25
|
+
end
|
26
|
+
delimiters = []
|
27
|
+
|
28
|
+
until @line.empty? || (delimiters.count == 0 && @line =~ end_re)
|
29
|
+
char = @line[0]
|
30
|
+
|
31
|
+
# backslash escaped delimiter
|
32
|
+
if char == '\\' && RUBY_ALL_DELIMITERS.include?(@line[1])
|
33
|
+
code << @line.slice!(0, 2)
|
34
|
+
next
|
35
|
+
end
|
36
|
+
|
37
|
+
case char
|
38
|
+
when RUBY_START_DELIMITERS_RE
|
39
|
+
if RUBY_NOT_NESTABLE_DELIMITERS.include?(char) && delimiters.last == char
|
40
|
+
# end char of not nestable delimiter
|
41
|
+
delimiters.pop
|
42
|
+
else
|
43
|
+
# diving
|
44
|
+
delimiters << char
|
45
|
+
end
|
46
|
+
|
47
|
+
when RUBY_END_DELIMITERS_RE
|
48
|
+
# rising
|
49
|
+
delimiters.pop if char == RUBY_DELIMITERS_REVERSE[delimiters.last]
|
50
|
+
end
|
51
|
+
|
52
|
+
code << @line.slice!(0)
|
53
|
+
end
|
54
|
+
|
55
|
+
syntax_error('Unexpected end of ruby code') unless delimiters.empty?
|
56
|
+
|
57
|
+
code.strip
|
58
|
+
end
|
59
|
+
|
60
|
+
RUBY_DELIMITERS_REVERSE = {
|
61
|
+
'(' => ')',
|
62
|
+
'[' => ']',
|
63
|
+
'{' => '}',
|
64
|
+
}.freeze
|
65
|
+
|
66
|
+
RUBY_QUOTES = %w(' ").freeze
|
67
|
+
|
68
|
+
RUBY_NOT_NESTABLE_DELIMITERS = RUBY_QUOTES
|
69
|
+
|
70
|
+
RUBY_START_DELIMITERS = (%w(\( [ {) + RUBY_NOT_NESTABLE_DELIMITERS).freeze
|
71
|
+
RUBY_END_DELIMITERS = (%w(\) ] }) + RUBY_NOT_NESTABLE_DELIMITERS).freeze
|
72
|
+
RUBY_ALL_DELIMITERS = (RUBY_START_DELIMITERS + RUBY_END_DELIMITERS).uniq.freeze
|
73
|
+
|
74
|
+
RUBY_START_DELIMITERS_RE = /\A[#{Regexp.escape RUBY_START_DELIMITERS.join}]/
|
75
|
+
RUBY_END_DELIMITERS_RE = /\A[#{Regexp.escape RUBY_END_DELIMITERS.join}]/
|
76
|
+
end
|
77
|
+
end
|