bade 0.2.0 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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