sade 0.1.0.pre
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 +7 -0
- data/LICENSE +24 -0
- data/README.md +14 -0
- data/VERSION +1 -0
- data/bin/sade +179 -0
- data/lib/sade/attr_block_reader.rb +67 -0
- data/lib/sade/attr_read_helper.rb +33 -0
- data/lib/sade/attr_shortcut_reader.rb +76 -0
- data/lib/sade/attr_value_reader.rb +50 -0
- data/lib/sade/composite_builder.rb +66 -0
- data/lib/sade/document.rb +23 -0
- data/lib/sade/document_iterator.rb +35 -0
- data/lib/sade/element_builder.rb +81 -0
- data/lib/sade/else_builder.rb +25 -0
- data/lib/sade/exception.rb +5 -0
- data/lib/sade/for_builder.rb +33 -0
- data/lib/sade/heredoc_builder.rb +27 -0
- data/lib/sade/heredoc_reader.rb +70 -0
- data/lib/sade/if_builder.rb +45 -0
- data/lib/sade/import_builder.rb +41 -0
- data/lib/sade/include_builder.rb +82 -0
- data/lib/sade/interpolation.rb +38 -0
- data/lib/sade/iterator.rb +49 -0
- data/lib/sade/lexer.rb +13 -0
- data/lib/sade/node.rb +12 -0
- data/lib/sade/parser.rb +134 -0
- data/lib/sade/parser_helper.rb +40 -0
- data/lib/sade/quoted_string_reader.rb +38 -0
- data/lib/sade/renderer.rb +185 -0
- data/lib/sade/token.rb +7 -0
- data/lib/sade/token_stream.rb +36 -0
- data/lib/sade/token_stream_iterator.rb +82 -0
- data/lib/sade/tokenizer.rb +250 -0
- data/lib/sade/version.rb +3 -0
- data/lib/sade.rb +21 -0
- metadata +79 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
require_relative 'parser_helper'
|
|
2
|
+
|
|
3
|
+
module Sade
|
|
4
|
+
class ForBuilder
|
|
5
|
+
attr_reader :parser
|
|
6
|
+
|
|
7
|
+
include ParserHelper
|
|
8
|
+
|
|
9
|
+
def initialize(parser) = @parser = parser
|
|
10
|
+
|
|
11
|
+
def build(it)
|
|
12
|
+
last_it = it.matching_rparen
|
|
13
|
+
|
|
14
|
+
return nil unless for_expr?(it)
|
|
15
|
+
error("')' that should match '(' is not found", it) if last_it.nil?
|
|
16
|
+
|
|
17
|
+
it.next.skip_space # ( とその後ろのスペースをスキップ
|
|
18
|
+
it.next.skip_space # %for とその後ろのスペースをスキップ
|
|
19
|
+
|
|
20
|
+
error("'#{it.val}' is not available as %for's first parameter.", it) unless it.symbol?
|
|
21
|
+
|
|
22
|
+
placeholder = it.val
|
|
23
|
+
it.next.skip_space # placeholderとその後ろのスペースをスキップ
|
|
24
|
+
|
|
25
|
+
error("the second parameter of %if must be variable", it) unless it.variable?
|
|
26
|
+
|
|
27
|
+
collection = Variable.new(it.val[1..-1])
|
|
28
|
+
it.next.skip_space # 変数とその後ろのスペースをスキップ
|
|
29
|
+
|
|
30
|
+
For.new(collection, placeholder, parser.read_children(it, last_it))
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
require_relative 'parser_helper'
|
|
2
|
+
require_relative 'interpolation'
|
|
3
|
+
|
|
4
|
+
module Sade
|
|
5
|
+
class HeredocBuilder
|
|
6
|
+
attr_reader :parser
|
|
7
|
+
|
|
8
|
+
include ParserHelper
|
|
9
|
+
include Interpolation
|
|
10
|
+
|
|
11
|
+
def initialize(parser) = @parser = parser
|
|
12
|
+
|
|
13
|
+
def build(it)
|
|
14
|
+
return nil unless it.heredoc?
|
|
15
|
+
return nil if it.val.size.zero?
|
|
16
|
+
|
|
17
|
+
val = it.val
|
|
18
|
+
return Heredoc.new([Text.new(val)]) unless include_interpolation?(val)
|
|
19
|
+
|
|
20
|
+
contents = parse_interpolated_text(val, it)
|
|
21
|
+
|
|
22
|
+
return nil if contents.empty?
|
|
23
|
+
|
|
24
|
+
Heredoc.new(contents)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
module Sade
|
|
2
|
+
class HeredocReader
|
|
3
|
+
attr_reader :doc, :itr
|
|
4
|
+
|
|
5
|
+
def read(doc, itr)
|
|
6
|
+
@doc = doc
|
|
7
|
+
@itr = itr
|
|
8
|
+
|
|
9
|
+
return nil unless delimiter?
|
|
10
|
+
|
|
11
|
+
indent_size = get_indent_size
|
|
12
|
+
indent = " " * indent_size
|
|
13
|
+
error %(a newline shold be placed after """) unless peek(3) == "\n"
|
|
14
|
+
|
|
15
|
+
step(3).newline # """の後ろの改行は内容に含めない
|
|
16
|
+
|
|
17
|
+
content = String.new
|
|
18
|
+
until self.end? || delimiter?
|
|
19
|
+
line = read_line
|
|
20
|
+
error "indent size doesn't match #{indent_size}" unless line[0...indent_size] == indent
|
|
21
|
+
content << line[indent_size..-1]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
error 'unterminated heredoc' unless delimiter?
|
|
25
|
+
error "indent size doesn't match #{indent_size}" unless indent_size == get_indent_size
|
|
26
|
+
error 'a newline shold be placed after """' unless peek(3) == "\n"
|
|
27
|
+
|
|
28
|
+
step(3) # """ の後ろの改行は消費しない
|
|
29
|
+
content
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def peek(offset) = itr.peek(offset)
|
|
35
|
+
def step(count) = itr.step(count)
|
|
36
|
+
def end? = itr.end?
|
|
37
|
+
|
|
38
|
+
def delimiter?
|
|
39
|
+
it = itr.clone
|
|
40
|
+
it.step(3)
|
|
41
|
+
doc.match?(itr, it, '"""')
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def read_line
|
|
45
|
+
it = itr.clone
|
|
46
|
+
itr.next until itr.end? || itr.current == "\n" || delimiter?
|
|
47
|
+
itr.newline if itr.current == "\n"
|
|
48
|
+
doc.substr(it, itr)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def error(msg)
|
|
52
|
+
error_msg = "%s: line %d col %d" % [msg, itr.line, itr.col]
|
|
53
|
+
raise LexerError, error_msg
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def get_indent_size
|
|
57
|
+
return 0 if itr.offset.zero?
|
|
58
|
+
|
|
59
|
+
decrement = 1
|
|
60
|
+
ch = itr.peek(-decrement)
|
|
61
|
+
until (itr.offset - decrement).negative? || ch == "\n"
|
|
62
|
+
error "indents must contain only spaces." unless ch == ' '
|
|
63
|
+
decrement += 1
|
|
64
|
+
ch = itr.peek(-decrement)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
decrement - 1
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
require_relative 'parser_helper'
|
|
2
|
+
|
|
3
|
+
module Sade
|
|
4
|
+
class IfBuilder
|
|
5
|
+
attr_reader :parser
|
|
6
|
+
|
|
7
|
+
include ParserHelper
|
|
8
|
+
|
|
9
|
+
def initialize(parser) = @parser = parser
|
|
10
|
+
|
|
11
|
+
def build(it)
|
|
12
|
+
last_it = it.matching_rparen
|
|
13
|
+
|
|
14
|
+
return nil unless if_expr?(it)
|
|
15
|
+
error("')' that should match '(' is not found", it) if last_it.nil?
|
|
16
|
+
|
|
17
|
+
it.next.skip_space # ( とその後ろのスペースをスキップ
|
|
18
|
+
it.next.skip_space # %if とその後ろのスペースをスキップ
|
|
19
|
+
|
|
20
|
+
error("the first parameter of %if must be variable", it) unless it.variable?
|
|
21
|
+
|
|
22
|
+
var = Variable.new(it.val[1..-1])
|
|
23
|
+
|
|
24
|
+
it.next.skip_space # 変数とその後ろのスペースをスキップ
|
|
25
|
+
|
|
26
|
+
then_nodes, else_nodes = read_then_and_else_nodes(it, last_it)
|
|
27
|
+
|
|
28
|
+
If.new(var, then_nodes, else_nodes)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def read_then_and_else_nodes(it, last_it)
|
|
32
|
+
then_nodes = []
|
|
33
|
+
else_nodes = []
|
|
34
|
+
|
|
35
|
+
then_nodes = parser.read_children(it, last_it) do |expr|
|
|
36
|
+
if expr.is_a?(Else)
|
|
37
|
+
else_nodes = expr.children
|
|
38
|
+
it.step(last_it.offset - it.offset) # (%else ...) の後に書かれたものは全無視
|
|
39
|
+
true
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
[then_nodes, else_nodes]
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
require_relative 'parser_helper'
|
|
2
|
+
|
|
3
|
+
module Sade
|
|
4
|
+
class ImportBuilder
|
|
5
|
+
attr_reader :parser
|
|
6
|
+
|
|
7
|
+
include ParserHelper
|
|
8
|
+
|
|
9
|
+
def initialize(parser) = @parser = parser
|
|
10
|
+
|
|
11
|
+
def build(it)
|
|
12
|
+
last_it = it.matching_rparen
|
|
13
|
+
variable_map = {}
|
|
14
|
+
|
|
15
|
+
return nil unless import_expr?(it)
|
|
16
|
+
error("')' that should match '(' is not found", it) if last_it.nil?
|
|
17
|
+
|
|
18
|
+
it.next.skip_space # ( とその後ろのスペースをスキップ
|
|
19
|
+
it.next.skip_space # %import とその後ろのスペースをスキップ
|
|
20
|
+
|
|
21
|
+
path = read_path(it, last_it)
|
|
22
|
+
error("file path is empty", it) if path&.size.zero?
|
|
23
|
+
|
|
24
|
+
it.move_at(last_it.offset) # イテレータ位置調整
|
|
25
|
+
|
|
26
|
+
content = read_content(path, it)
|
|
27
|
+
return nil if content.size.zero?
|
|
28
|
+
|
|
29
|
+
Import.new(path, content)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def read_content(path, it)
|
|
33
|
+
content = ''
|
|
34
|
+
File.open("#{parser.import_path}/#{path}"){ |f| content =f.read }
|
|
35
|
+
content = content[0..-2] if content[-1] == "\n"
|
|
36
|
+
content
|
|
37
|
+
rescue Errno::ENOENT => e
|
|
38
|
+
error(e.message, it)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
require_relative 'parser_helper'
|
|
2
|
+
|
|
3
|
+
module Sade
|
|
4
|
+
class IncludeBuilder
|
|
5
|
+
attr_reader :parser
|
|
6
|
+
|
|
7
|
+
include ParserHelper
|
|
8
|
+
|
|
9
|
+
def initialize(parser) = @parser = parser
|
|
10
|
+
|
|
11
|
+
def build(it)
|
|
12
|
+
last_it = it.matching_rparen
|
|
13
|
+
variable_map = {}
|
|
14
|
+
|
|
15
|
+
return nil unless include_expr?(it)
|
|
16
|
+
error("')' that should match '(' is not found", it) if last_it.nil?
|
|
17
|
+
|
|
18
|
+
it.next.skip_space # ( とその後ろのスペースをスキップ
|
|
19
|
+
it.next.skip_space # %include とその後ろのスペースをスキップ
|
|
20
|
+
|
|
21
|
+
path = read_path(it, last_it)
|
|
22
|
+
error("file path is empty", it) if path&.size.zero?
|
|
23
|
+
|
|
24
|
+
if end_of_tag?(it, last_it)
|
|
25
|
+
children = read_content(path, it)
|
|
26
|
+
return children.size == 0 ? nil : Include.new(path, variable_map, children)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it.next.skip_space # path とその後ろのスペースをスキップ
|
|
30
|
+
|
|
31
|
+
variable_map = read_variable_map(it) || {}
|
|
32
|
+
it.move_at(last_it.offset) # イテレータ位置調整
|
|
33
|
+
|
|
34
|
+
children = read_content(path, it)
|
|
35
|
+
return nil if children.size.zero?
|
|
36
|
+
|
|
37
|
+
Include.new(path, variable_map, children)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def read_content(path, it)
|
|
41
|
+
content = ''
|
|
42
|
+
File.open("#{parser.include_path}/#{path}"){ |f| content =f.read }
|
|
43
|
+
content = content[0..-2] if content[-1] == "\n"
|
|
44
|
+
token_stream = Lexer.new.lex(content)
|
|
45
|
+
parser.parse(token_stream)
|
|
46
|
+
rescue Errno::ENOENT => e
|
|
47
|
+
error(e.message, it)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def read_path(it, last_it)
|
|
51
|
+
return it.val if it.text?
|
|
52
|
+
|
|
53
|
+
path = String.new
|
|
54
|
+
loop do
|
|
55
|
+
break if it.eof?
|
|
56
|
+
break if end_of_tag?(it, last_it)
|
|
57
|
+
|
|
58
|
+
path << it.val
|
|
59
|
+
|
|
60
|
+
break if it.peek(1).type == :space
|
|
61
|
+
|
|
62
|
+
it.next
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
path
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def read_variable_map(it)
|
|
69
|
+
return {} unless it.lbrace?
|
|
70
|
+
|
|
71
|
+
map = parser.attr_block_reader.read(it)
|
|
72
|
+
|
|
73
|
+
return {} if map.nil? || map.empty?
|
|
74
|
+
|
|
75
|
+
map.each do |key, node|
|
|
76
|
+
error("the value of the key '#{key}' must be variable", it) unless node.is_a?(Variable)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
map
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
module Sade
|
|
2
|
+
module Interpolation
|
|
3
|
+
# 「補完が含まれているか」だけを判定する軽いフィルタ
|
|
4
|
+
def include_interpolation?(text) = text.index('${')
|
|
5
|
+
|
|
6
|
+
# text 内の ${...} をすべて走査し、
|
|
7
|
+
# Text / Variable のフラットなノード配列を返す
|
|
8
|
+
def parse_interpolated_text(text, it)
|
|
9
|
+
nodes = []
|
|
10
|
+
pos = 0
|
|
11
|
+
len = text.size
|
|
12
|
+
|
|
13
|
+
while (start = text.index('${', pos))
|
|
14
|
+
# ${ の前にテキストがあれば Text として追加
|
|
15
|
+
if start > pos
|
|
16
|
+
nodes << Text.new(text[pos...start])
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
end_pos = text.index('}', start + 2)
|
|
20
|
+
error('"}" not found for interpolation', it) if end_pos.nil?
|
|
21
|
+
|
|
22
|
+
variable_name = text[(start + 2)...end_pos].strip
|
|
23
|
+
error('variable name not found between ${ and }', it) if variable_name.size.zero?
|
|
24
|
+
|
|
25
|
+
nodes << Variable.new(variable_name)
|
|
26
|
+
|
|
27
|
+
pos = end_pos + 1
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# 残りのテキストがあれば Text として追加
|
|
31
|
+
if pos < len
|
|
32
|
+
nodes << Text.new(text[pos...len])
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
nodes
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
module Sade
|
|
2
|
+
class Iterator
|
|
3
|
+
State = Struct.new(:buf, :offset)
|
|
4
|
+
|
|
5
|
+
attr_reader :state
|
|
6
|
+
|
|
7
|
+
def initialize(buf, offset)
|
|
8
|
+
@state = State.new(buf: buf, offset: offset)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def offset = state.offset
|
|
12
|
+
def begin? = state.offset.zero?
|
|
13
|
+
def end? = state.offset >= buf_size
|
|
14
|
+
|
|
15
|
+
def current
|
|
16
|
+
self.end? ? nil : state.buf[state.offset]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def next
|
|
20
|
+
state.offset += 1 unless self.end?
|
|
21
|
+
self
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def step(count)
|
|
25
|
+
if (state.offset + count).negative?
|
|
26
|
+
state.offset = 0
|
|
27
|
+
elsif state.offset + count >= buf_size
|
|
28
|
+
state.offset = buf_size
|
|
29
|
+
else
|
|
30
|
+
state.offset += count
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
self
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def peek(count)
|
|
37
|
+
return state.buf[-1] if state.offset + count >= buf_size
|
|
38
|
+
return state.buf[0] if (state.offset + count).negative?
|
|
39
|
+
|
|
40
|
+
state.buf[state.offset + count]
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def clone = self.class.new(state.buf, state.offset)
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def buf_size = state.buf.size
|
|
48
|
+
end
|
|
49
|
+
end
|
data/lib/sade/lexer.rb
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
require_relative 'exception'
|
|
2
|
+
require_relative 'token'
|
|
3
|
+
require_relative 'document'
|
|
4
|
+
require_relative 'tokenizer'
|
|
5
|
+
|
|
6
|
+
module Sade
|
|
7
|
+
class Lexer
|
|
8
|
+
def lex(string, debug: false)
|
|
9
|
+
doc = Document.new(string)
|
|
10
|
+
Tokenizer.new(doc, debug: debug).run
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
data/lib/sade/node.rb
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
module Sade
|
|
2
|
+
Element = Struct.new(:name, :attrs, :children)
|
|
3
|
+
Text = Struct.new(:value)
|
|
4
|
+
Variable = Struct.new(:key)
|
|
5
|
+
Composite = Struct.new(:contents)
|
|
6
|
+
Heredoc = Struct.new(:contents)
|
|
7
|
+
If = Struct.new(:condition, :then_node, :else_node)
|
|
8
|
+
Else = Struct.new(:children)
|
|
9
|
+
For = Struct.new(:collection, :placeholder, :body)
|
|
10
|
+
Include = Struct.new(:path, :variable_map, :children)
|
|
11
|
+
Import = Struct.new(:path, :content)
|
|
12
|
+
end
|
data/lib/sade/parser.rb
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
require_relative 'parser_helper'
|
|
2
|
+
require_relative 'composite_builder'
|
|
3
|
+
require_relative 'attr_value_reader'
|
|
4
|
+
require_relative 'attr_shortcut_reader'
|
|
5
|
+
require_relative 'attr_block_reader'
|
|
6
|
+
require_relative 'element_builder'
|
|
7
|
+
require_relative 'if_builder'
|
|
8
|
+
require_relative 'else_builder'
|
|
9
|
+
require_relative 'for_builder'
|
|
10
|
+
require_relative 'include_builder'
|
|
11
|
+
require_relative 'import_builder'
|
|
12
|
+
require_relative 'heredoc_builder'
|
|
13
|
+
|
|
14
|
+
module Sade
|
|
15
|
+
class Parser
|
|
16
|
+
attr_reader :attr_shortcut_reader,
|
|
17
|
+
:attr_value_reader,
|
|
18
|
+
:attr_block_reader,
|
|
19
|
+
:composite_builder,
|
|
20
|
+
:element_builder,
|
|
21
|
+
:if_builder,
|
|
22
|
+
:else_builder,
|
|
23
|
+
:for_builder,
|
|
24
|
+
:include_builder,
|
|
25
|
+
:import_builder,
|
|
26
|
+
:heredoc_builder,
|
|
27
|
+
:include_path,
|
|
28
|
+
:import_path
|
|
29
|
+
|
|
30
|
+
include ParserHelper
|
|
31
|
+
|
|
32
|
+
def initialize(include_path: ".", import_path: ".")
|
|
33
|
+
@composite_builder = CompositeBuilder.new
|
|
34
|
+
@attr_value_reader = AttrValueReader.new(self)
|
|
35
|
+
@attr_shortcut_reader = AttrShortcutReader.new(self)
|
|
36
|
+
@attr_block_reader = AttrBlockReader.new(self)
|
|
37
|
+
@element_builder = ElementBuilder.new(self)
|
|
38
|
+
@if_builder = IfBuilder.new(self)
|
|
39
|
+
@else_builder = ElseBuilder.new(self)
|
|
40
|
+
@for_builder = ForBuilder.new(self)
|
|
41
|
+
@include_builder = IncludeBuilder.new(self)
|
|
42
|
+
@import_builder = ImportBuilder.new(self)
|
|
43
|
+
@heredoc_builder = HeredocBuilder.new(self)
|
|
44
|
+
@include_path = include_path == '.' ? File.expand_path('.') : include_path
|
|
45
|
+
@import_path = import_path == '.' ? File.expand_path('.') : import_path
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def parse(token_stream)
|
|
50
|
+
it = token_stream.begin
|
|
51
|
+
nodes = []
|
|
52
|
+
|
|
53
|
+
return [] if space_all?(it)
|
|
54
|
+
|
|
55
|
+
until it.eof?
|
|
56
|
+
composite = read_text(it)
|
|
57
|
+
nodes.push(composite) unless composite.nil?
|
|
58
|
+
|
|
59
|
+
break if it.eof?
|
|
60
|
+
|
|
61
|
+
node = read_next_node(it)
|
|
62
|
+
nodes.push(node) unless node.nil?
|
|
63
|
+
it.next unless it.eof?
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
nodes
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def next_node_type(it)
|
|
70
|
+
return :heredoc if it.heredoc?
|
|
71
|
+
return :text unless it.lparen?
|
|
72
|
+
return :if if if_expr?(it)
|
|
73
|
+
return :else if else_expr?(it)
|
|
74
|
+
return :for if for_expr?(it)
|
|
75
|
+
return :include if include_expr?(it)
|
|
76
|
+
return :import if import_expr?(it)
|
|
77
|
+
|
|
78
|
+
:element
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def read_next_node(it)
|
|
82
|
+
case next_node_type(it)
|
|
83
|
+
when :text
|
|
84
|
+
composite_builder.build(it)
|
|
85
|
+
when :heredoc
|
|
86
|
+
read_heredoc(it)
|
|
87
|
+
when :element
|
|
88
|
+
element_builder.build(it)
|
|
89
|
+
when :if
|
|
90
|
+
if_builder.build(it)
|
|
91
|
+
when :else
|
|
92
|
+
else_builder.build(it)
|
|
93
|
+
when :for
|
|
94
|
+
for_builder.build(it)
|
|
95
|
+
when :include
|
|
96
|
+
include_builder.build(it)
|
|
97
|
+
when :import
|
|
98
|
+
import_builder.build(it)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def read_heredoc(it) = heredoc_builder.build(it)
|
|
103
|
+
|
|
104
|
+
def read_children(it, last_it, &else_check)
|
|
105
|
+
children = []
|
|
106
|
+
until it.eof? || end_of_tag?(it, last_it)
|
|
107
|
+
composite = read_text(it, has_parent: true)
|
|
108
|
+
children << composite unless composite.nil?
|
|
109
|
+
|
|
110
|
+
break if end_of_tag?(it, last_it)
|
|
111
|
+
|
|
112
|
+
node = read_next_node(it)
|
|
113
|
+
|
|
114
|
+
else_node_found = false
|
|
115
|
+
else_node_found = else_check.call(node) if block_given?
|
|
116
|
+
|
|
117
|
+
break if else_node_found
|
|
118
|
+
|
|
119
|
+
children << node unless node.nil?
|
|
120
|
+
it.next unless end_of_tag?(it, last_it)
|
|
121
|
+
end
|
|
122
|
+
children
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def read_text(it, has_parent: false) = composite_builder.build(it, has_parent: has_parent)
|
|
126
|
+
|
|
127
|
+
def space_all?(it)
|
|
128
|
+
_it = it.clone
|
|
129
|
+
_it.next while _it.space?
|
|
130
|
+
|
|
131
|
+
_it.eof?
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
module Sade
|
|
2
|
+
module ParserHelper
|
|
3
|
+
def eof_error(it) = error("unexpected eof", it)
|
|
4
|
+
def error(msg, it) = raise Sade::ParseError, "%s: line %d col %d" % [msg, it.line, it.col]
|
|
5
|
+
def if_expr?(it) = control_expr?(it, '%if')
|
|
6
|
+
def else_expr?(it) = control_expr?(it, '%else')
|
|
7
|
+
def for_expr?(it) = control_expr?(it, '%for')
|
|
8
|
+
def include_expr?(it) = control_expr?(it, '%include')
|
|
9
|
+
def import_expr?(it) = control_expr?(it, '%import')
|
|
10
|
+
|
|
11
|
+
def control_expr?(iterator, type)
|
|
12
|
+
it = iterator.clone
|
|
13
|
+
return false unless it.lparen?
|
|
14
|
+
|
|
15
|
+
it.next.skip_space
|
|
16
|
+
|
|
17
|
+
it.val == type
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def end_of_tag?(it, last_it) = it.offset == last_it.offset
|
|
21
|
+
|
|
22
|
+
def read_path(it, last_it)
|
|
23
|
+
return it.val if it.text?
|
|
24
|
+
|
|
25
|
+
path = String.new
|
|
26
|
+
loop do
|
|
27
|
+
break if it.eof?
|
|
28
|
+
break if end_of_tag?(it, last_it)
|
|
29
|
+
|
|
30
|
+
path << it.val
|
|
31
|
+
|
|
32
|
+
break if it.peek(1).type == :space
|
|
33
|
+
|
|
34
|
+
it.next
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
path
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
module Sade
|
|
2
|
+
class QuotedStringReader
|
|
3
|
+
attr_reader :itr, :delimiter
|
|
4
|
+
|
|
5
|
+
def read(itr, delimiter)
|
|
6
|
+
return nil unless itr.current == delimiter
|
|
7
|
+
|
|
8
|
+
buf = String.new
|
|
9
|
+
first_itr = itr.clone
|
|
10
|
+
last_itr = first_itr.next.clone
|
|
11
|
+
until last_itr.end?
|
|
12
|
+
ch = last_itr.current
|
|
13
|
+
case ch
|
|
14
|
+
when delimiter
|
|
15
|
+
if last_itr.peek(-1) == "\\"
|
|
16
|
+
buf[-1] = ch
|
|
17
|
+
else
|
|
18
|
+
break
|
|
19
|
+
end
|
|
20
|
+
else
|
|
21
|
+
buf << last_itr.current unless last_itr.end?
|
|
22
|
+
end
|
|
23
|
+
last_itr.next
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
error('Unterminated string literal', itr) unless last_itr.current == delimiter
|
|
27
|
+
|
|
28
|
+
itr.step(last_itr.offset - itr.offset + 1)
|
|
29
|
+
|
|
30
|
+
buf
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def error(msg, itr)
|
|
34
|
+
error_msg = "%s: line %d col %d" % [msg, itr.line, itr.col]
|
|
35
|
+
raise LexerError, error_msg
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|