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.
@@ -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
@@ -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