bade 0.2.0 → 0.2.1
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 +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
|