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
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 9807167655e58ad5e58b785bc420b4135a435759376185086d9da0a5ec4d15d8
|
|
4
|
+
data.tar.gz: aa10c7e97c88928a943371aaedcb8a3e6df6b8a326cb5ab893af9483728217a3
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 619cd3fca2000ad0dab421dc020b888d72ce55d72495fb46b2c0ab5a73c0271d6051cd29bd442aa2e48a5fa89d494b3700124777f9a1567632fd19dd3032477b
|
|
7
|
+
data.tar.gz: 9fe60a2aa87cf1662cdf2e337be27caf3a9ccff2d12a6758d2d0248c51ebc31115f7ad67666e86ef955982095f04cd167c323279e917f52192cfe7ee9a4d0c96
|
data/LICENSE
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
BSD 2-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, Takanobu Maekawa
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
16
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
17
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
18
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
19
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
20
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
21
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
22
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
23
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
24
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
data/README.md
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Sade
|
|
2
|
+
|
|
3
|
+
An HTML template language for static page generation.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
Sade is a template language designed to make writing HTML easier and more readable.
|
|
7
|
+
|
|
8
|
+
- It uses an S-expression based syntax, which requires parentheses, but allows flexible
|
|
9
|
+
indentation and line breaks.
|
|
10
|
+
|
|
11
|
+
- Templates and raw HTML can be embedded directly, making reuse straightforward.
|
|
12
|
+
|
|
13
|
+
- Additional shorthand notations help reduce the number of
|
|
14
|
+
parentheses, and the id/class shortcuts follow the style popularized by Haml.
|
data/VERSION
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.1.0.pre
|
data/bin/sade
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "optparse"
|
|
5
|
+
require "json"
|
|
6
|
+
require "sade"
|
|
7
|
+
require "sade/version"
|
|
8
|
+
|
|
9
|
+
#
|
|
10
|
+
# Struct → JSON 変換(AST 用)
|
|
11
|
+
#
|
|
12
|
+
def ast_to_h(node)
|
|
13
|
+
case node
|
|
14
|
+
when Struct
|
|
15
|
+
{
|
|
16
|
+
"__type__" => node.class.name.split("::").last,
|
|
17
|
+
**node.to_h.transform_values { |v| ast_to_h(v) }
|
|
18
|
+
}
|
|
19
|
+
when Array
|
|
20
|
+
node.map { |v| ast_to_h(v) }
|
|
21
|
+
else
|
|
22
|
+
node
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
#
|
|
27
|
+
# CLI オプション
|
|
28
|
+
#
|
|
29
|
+
options = {
|
|
30
|
+
mode: :render,
|
|
31
|
+
escape: true,
|
|
32
|
+
doctype: false,
|
|
33
|
+
include_path: ".",
|
|
34
|
+
import_path: ".",
|
|
35
|
+
context_json: nil,
|
|
36
|
+
context_file: nil
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
parser = OptionParser.new do |opt|
|
|
40
|
+
opt.banner = "Usage: sade [options] < input.sd > output.html"
|
|
41
|
+
|
|
42
|
+
opt.on("-l", "--lex", "Run lexer only (JSON output)") do
|
|
43
|
+
options[:mode] = :lex
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
opt.on("-p", "--parse", "Run parser only (JSON output)") do
|
|
47
|
+
options[:mode] = :parse
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
opt.on("--noescape", "Disable HTML escaping") do
|
|
51
|
+
options[:escape] = false
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
opt.on("-d", "--doctype", "Add <!DOCTYPE html> to output") do
|
|
55
|
+
options[:doctype] = true
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
opt.on("--include-path PATH", "Base directory for %include") do |v|
|
|
59
|
+
options[:include_path] = v
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
opt.on("--import-path PATH", "Base directory for %import") do |v|
|
|
63
|
+
options[:import_path] = v
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
opt.on("--context-json JSON", "Inline JSON context") do |v|
|
|
67
|
+
options[:context_json] = v
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
opt.on("--context-file PATH", "Load JSON context from file") do |v|
|
|
71
|
+
options[:context_file] = v
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
opt.on("-v", "--version") do
|
|
75
|
+
puts Sade::VERSION
|
|
76
|
+
exit(0)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
opt.on("-h", "--help", "Show help") do
|
|
80
|
+
puts opt
|
|
81
|
+
exit 0
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
begin
|
|
86
|
+
parser.parse!
|
|
87
|
+
rescue OptionParser::InvalidOption => e
|
|
88
|
+
warn e.message
|
|
89
|
+
warn parser
|
|
90
|
+
exit 1
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
#
|
|
94
|
+
# context の排他チェック
|
|
95
|
+
#
|
|
96
|
+
if options[:context_json] && options[:context_file]
|
|
97
|
+
warn "Error: --context-json と --context-file は同時に指定できません"
|
|
98
|
+
exit 1
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
#
|
|
102
|
+
# context の読み込み
|
|
103
|
+
#
|
|
104
|
+
context = {}
|
|
105
|
+
|
|
106
|
+
if options[:context_json]
|
|
107
|
+
begin
|
|
108
|
+
context = JSON.parse(options[:context_json])
|
|
109
|
+
rescue JSON::ParserError => e
|
|
110
|
+
warn "JSON parse error in --context-json: #{e.message}"
|
|
111
|
+
exit 1
|
|
112
|
+
end
|
|
113
|
+
elsif options[:context_file]
|
|
114
|
+
begin
|
|
115
|
+
context = JSON.parse(File.read(options[:context_file]))
|
|
116
|
+
rescue Errno::ENOENT
|
|
117
|
+
warn "JSON file not found: #{options[:context_file]}"
|
|
118
|
+
exit 1
|
|
119
|
+
rescue JSON::ParserError => e
|
|
120
|
+
warn "JSON parse error in file #{options[:context_file]}: #{e.message}"
|
|
121
|
+
exit 1
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
#
|
|
126
|
+
# 標準入力を読む
|
|
127
|
+
#
|
|
128
|
+
input = STDIN.read
|
|
129
|
+
|
|
130
|
+
#
|
|
131
|
+
# Engine 準備
|
|
132
|
+
#
|
|
133
|
+
engine = Sade::Engine.new(
|
|
134
|
+
include_path: options[:include_path],
|
|
135
|
+
import_path: options[:import_path]
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
lexer = engine.lexer
|
|
139
|
+
parser = engine.parser
|
|
140
|
+
|
|
141
|
+
#
|
|
142
|
+
# lex モード
|
|
143
|
+
#
|
|
144
|
+
if options[:mode] == :lex
|
|
145
|
+
token_stream = lexer.lex(input)
|
|
146
|
+
it = token_stream.begin
|
|
147
|
+
until it.eof?
|
|
148
|
+
puts JSON.pretty_generate(it.current)
|
|
149
|
+
it.next
|
|
150
|
+
end
|
|
151
|
+
exit 0
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
#
|
|
155
|
+
# parse モード
|
|
156
|
+
#
|
|
157
|
+
if options[:mode] == :parse
|
|
158
|
+
tokens = lexer.lex(input)
|
|
159
|
+
ast = parser.parse(tokens)
|
|
160
|
+
puts JSON.pretty_generate(ast_to_h(ast))
|
|
161
|
+
exit 0
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
#
|
|
165
|
+
# render モード(デフォルト)
|
|
166
|
+
#
|
|
167
|
+
begin
|
|
168
|
+
html = engine.render(
|
|
169
|
+
input,
|
|
170
|
+
context,
|
|
171
|
+
escape: options[:escape],
|
|
172
|
+
doctype: options[:doctype]
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
puts html
|
|
176
|
+
rescue => e
|
|
177
|
+
puts e.message
|
|
178
|
+
exit(1)
|
|
179
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
require_relative 'attr_read_helper'
|
|
2
|
+
require_relative 'parser_helper'
|
|
3
|
+
|
|
4
|
+
module Sade
|
|
5
|
+
class AttrBlockReader
|
|
6
|
+
attr_reader :parser
|
|
7
|
+
|
|
8
|
+
include AttrReadHelper
|
|
9
|
+
include ParserHelper
|
|
10
|
+
|
|
11
|
+
def initialize(parser) = @parser = parser
|
|
12
|
+
|
|
13
|
+
def read(it)
|
|
14
|
+
return nil unless it.lbrace?
|
|
15
|
+
|
|
16
|
+
attrs = {}
|
|
17
|
+
|
|
18
|
+
it.next
|
|
19
|
+
|
|
20
|
+
while not it.rbrace?
|
|
21
|
+
eof_error(it) if it.eof?
|
|
22
|
+
|
|
23
|
+
it.skip_space
|
|
24
|
+
|
|
25
|
+
name = read_attr_name(it)
|
|
26
|
+
|
|
27
|
+
it.skip_space
|
|
28
|
+
|
|
29
|
+
val = read_attr_val(it)
|
|
30
|
+
|
|
31
|
+
merge_attrs(attrs, name, val) if name && val
|
|
32
|
+
|
|
33
|
+
it.skip_space
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
return nil if attrs.empty?
|
|
37
|
+
|
|
38
|
+
attrs
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def read_attr_name(it)
|
|
42
|
+
name = String.new
|
|
43
|
+
until it.rbrace? || it.space? || it.eof? do
|
|
44
|
+
type = it.type
|
|
45
|
+
val = it.val
|
|
46
|
+
error("'#{val}' cannot be included as the attribute name", it) unless valid_name_token?(type)
|
|
47
|
+
|
|
48
|
+
name << val
|
|
49
|
+
it.next
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
error("collon(:) should be placed at the end of '#{name}'", it) if name[-1] !=":"
|
|
53
|
+
|
|
54
|
+
name.chop.to_sym
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def read_attr_val(it) = parser.attr_value_reader.read(it)
|
|
58
|
+
|
|
59
|
+
def merge_attrs(attrs, name, node)
|
|
60
|
+
if name == :class
|
|
61
|
+
attrs[:class] = attrs.fetch(:class, []) + [node]
|
|
62
|
+
else
|
|
63
|
+
attrs[name] = node
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
module Sade
|
|
2
|
+
module AttrReadHelper
|
|
3
|
+
def unpermitted_attr_name_token_types
|
|
4
|
+
@unpermitted_attr_name_token_types ||= Set.new %i[
|
|
5
|
+
lparen rparen lbrace rbrace equal lt gt
|
|
6
|
+
]
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def unpermitted_attr_val_token_types
|
|
10
|
+
@unpermitted_attr_val_token_types ||= Set.new %i[
|
|
11
|
+
space lparen rparen lbrace rbrace eof
|
|
12
|
+
]
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def unpermitted_attr_val_token_types_for_shortcut
|
|
16
|
+
@unpermitted_attr_val_token_types_for_shortcut ||= Set.new %i[
|
|
17
|
+
dot hash space lparen rparen lbrace rbrace eof slash
|
|
18
|
+
]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def valid_name_token?(type)
|
|
22
|
+
not unpermitted_attr_name_token_types.include?(type)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def valid_value_token?(type)
|
|
26
|
+
not unpermitted_attr_val_token_types.include?(type)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def valid_value_token_for_shortcut?(type)
|
|
30
|
+
not unpermitted_attr_val_token_types_for_shortcut.include?(type)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
require_relative 'attr_read_helper'
|
|
2
|
+
require_relative 'parser_helper'
|
|
3
|
+
|
|
4
|
+
module Sade
|
|
5
|
+
class AttrShortcutReader
|
|
6
|
+
attr_reader :parser
|
|
7
|
+
|
|
8
|
+
include AttrReadHelper
|
|
9
|
+
include ParserHelper
|
|
10
|
+
|
|
11
|
+
def initialize(parser) = @parser = parser
|
|
12
|
+
|
|
13
|
+
def read(it, aditional_delimiters: [])
|
|
14
|
+
attrs = {}
|
|
15
|
+
|
|
16
|
+
while (hash?(it) || dot?(it)) && valid_attr_val?(it)
|
|
17
|
+
read_id(it, attrs) if hash?(it)
|
|
18
|
+
read_class(it, attrs) if dot?(it)
|
|
19
|
+
skip_space(it) if next_shortcut?(it)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
return nil if attrs.empty?
|
|
23
|
+
|
|
24
|
+
attrs
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def hash?(it) = it.hash?
|
|
28
|
+
def dot?(it) = it.dot?
|
|
29
|
+
def eof?(it) = it.eof?
|
|
30
|
+
def space?(it) = it.space?
|
|
31
|
+
|
|
32
|
+
def forward(it) = it.next
|
|
33
|
+
def skip_space(it) = it.skip_space
|
|
34
|
+
|
|
35
|
+
def valid_attr_val?(it)
|
|
36
|
+
next_token = it.peek(1)
|
|
37
|
+
val = next_token.val
|
|
38
|
+
type = next_token.type
|
|
39
|
+
|
|
40
|
+
return false if %i[space eof].include?(type)
|
|
41
|
+
|
|
42
|
+
tmp = it.clone
|
|
43
|
+
tmp.next
|
|
44
|
+
return true if if_expr?(tmp) || for_expr?(tmp)
|
|
45
|
+
|
|
46
|
+
unless valid_value_token_for_shortcut?(type)
|
|
47
|
+
error("'#{val}' is not expected after '#{it.val}'", it)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
true
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def next_shortcut?(it)
|
|
54
|
+
_it = it.clone
|
|
55
|
+
_it.next while _it.space?
|
|
56
|
+
|
|
57
|
+
_it.hash? || _it.dot?
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def read_id(it, attrs)
|
|
61
|
+
forward(it)
|
|
62
|
+
val = read_attr_val(it)
|
|
63
|
+
attrs[:id] = val unless val.nil?
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def read_class(it, attrs)
|
|
67
|
+
forward(it)
|
|
68
|
+
val = read_attr_val(it)
|
|
69
|
+
attrs[:class] = attrs.fetch(:class, []).push(val) unless val.nil?
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def read_attr_val(it)
|
|
73
|
+
parser.attr_value_reader.read(it, additional_delimiters: %i[dot hash slash lbrace rparen])
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
require_relative 'attr_read_helper'
|
|
2
|
+
require_relative 'parser_helper'
|
|
3
|
+
|
|
4
|
+
module Sade
|
|
5
|
+
class AttrValueReader
|
|
6
|
+
attr_reader :parser
|
|
7
|
+
|
|
8
|
+
include AttrReadHelper
|
|
9
|
+
include ParserHelper
|
|
10
|
+
|
|
11
|
+
def initialize(parser) = @parser = parser
|
|
12
|
+
|
|
13
|
+
def read(it, additional_delimiters: [])
|
|
14
|
+
error("attribute value not found", it) if it.rbrace? || it.eof?
|
|
15
|
+
|
|
16
|
+
if if_expr?(it)
|
|
17
|
+
node = parser.if_builder.build(it)
|
|
18
|
+
it.next
|
|
19
|
+
return node
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
if for_expr?(it)
|
|
23
|
+
node = parser.for_builder.build(it)
|
|
24
|
+
it.next
|
|
25
|
+
return node
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
if it.heredoc?
|
|
29
|
+
heredoc = parser.heredoc_builder.build(it)
|
|
30
|
+
it.next
|
|
31
|
+
return heredoc
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
delimiters = %i[space rbrace].concat(additional_delimiters)
|
|
35
|
+
composite =
|
|
36
|
+
parser.composite_builder.build(it, additional_delimiters: delimiters) do |_it|
|
|
37
|
+
if not valid_value_token?(_it.type)
|
|
38
|
+
error("#{_it.val} cannot be included as the attribute value", _it)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
return nil if composite.nil?
|
|
43
|
+
return nil if composite.contents.empty?
|
|
44
|
+
|
|
45
|
+
return composite.contents.first if composite.contents.size == 1
|
|
46
|
+
|
|
47
|
+
composite
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
require_relative 'node'
|
|
2
|
+
require_relative 'exception'
|
|
3
|
+
require_relative 'interpolation'
|
|
4
|
+
require_relative 'parser_helper'
|
|
5
|
+
|
|
6
|
+
module Sade
|
|
7
|
+
class CompositeBuilder
|
|
8
|
+
include ParserHelper
|
|
9
|
+
include Interpolation
|
|
10
|
+
|
|
11
|
+
def build(itr, has_parent: false, additional_delimiters: [], &validator)
|
|
12
|
+
|
|
13
|
+
str = String.new
|
|
14
|
+
buf = []
|
|
15
|
+
last_token_type = nil
|
|
16
|
+
delimiters = %i[eof lparen heredoc] + additional_delimiters
|
|
17
|
+
|
|
18
|
+
until delimiters.include?(itr.type)
|
|
19
|
+
break if itr.type == :rparen && has_parent
|
|
20
|
+
|
|
21
|
+
validator.call(itr) if block_given?
|
|
22
|
+
|
|
23
|
+
val = itr.val
|
|
24
|
+
|
|
25
|
+
if itr.variable?
|
|
26
|
+
buf.push(Text.new(str)) if str.size.positive?
|
|
27
|
+
buf.push(Variable.new(val[1..-1]))
|
|
28
|
+
str = String.new
|
|
29
|
+
|
|
30
|
+
elsif itr.text? && include_interpolation?(val)
|
|
31
|
+
# ここでいったんバッファ中の素のテキストを flush
|
|
32
|
+
buf.push(Text.new(str)) if str.size.positive?
|
|
33
|
+
|
|
34
|
+
# 補完入りテキストをフラットなノード列に分解して buf に展開
|
|
35
|
+
parse_interpolated_text(val, itr).each do |node|
|
|
36
|
+
buf << node
|
|
37
|
+
end
|
|
38
|
+
str = String.new
|
|
39
|
+
|
|
40
|
+
else
|
|
41
|
+
str << val unless last_token_type == :space && itr.space?
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
last_token_type = itr.type
|
|
45
|
+
itr.next
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# 親タグ式から呼び出されて ) で閉じずにEOFの場合、例外を出す必要がある
|
|
49
|
+
eof_error(itr) if itr.eof? && has_parent
|
|
50
|
+
|
|
51
|
+
# 親タグ式から呼び出されて ) の前にスペーストークンがあるなら削除
|
|
52
|
+
tmp = if last_token_type == :space && itr.type == :rparen && has_parent
|
|
53
|
+
str.chop
|
|
54
|
+
else
|
|
55
|
+
str
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
buf.push(Text.new(tmp)) if tmp.size.positive?
|
|
59
|
+
|
|
60
|
+
return nil if buf.size.zero?
|
|
61
|
+
|
|
62
|
+
Composite.new(buf)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
require_relative 'document_iterator'
|
|
2
|
+
|
|
3
|
+
module Sade
|
|
4
|
+
class Document
|
|
5
|
+
attr_reader :buff
|
|
6
|
+
|
|
7
|
+
def initialize(string)
|
|
8
|
+
@buff = string
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def begin
|
|
12
|
+
DocumentIterator.new(buff, 0, 1, 1)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def substr(first, last)
|
|
16
|
+
buff[first.offset...last.offset]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def match?(first, last, str)
|
|
20
|
+
buff[first.offset...last.offset] == str
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
require_relative 'iterator'
|
|
2
|
+
|
|
3
|
+
module Sade
|
|
4
|
+
class DocumentIterator < Iterator
|
|
5
|
+
attr_reader :line, :col
|
|
6
|
+
|
|
7
|
+
def initialize(buf, offset, line = 1, col = 1)
|
|
8
|
+
super(buf, offset)
|
|
9
|
+
@line = line
|
|
10
|
+
@col = col
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def newline
|
|
14
|
+
self.next
|
|
15
|
+
@line += 1
|
|
16
|
+
@col = 1
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def next
|
|
20
|
+
super
|
|
21
|
+
@col += 1
|
|
22
|
+
self
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def step(count)
|
|
26
|
+
super
|
|
27
|
+
@col += count
|
|
28
|
+
self
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def clone
|
|
32
|
+
self.class.new(state.buf, state.offset, line, col)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
require_relative 'node'
|
|
2
|
+
require_relative 'token'
|
|
3
|
+
require_relative 'parser_helper'
|
|
4
|
+
|
|
5
|
+
module Sade
|
|
6
|
+
class ElementBuilder
|
|
7
|
+
attr_reader :parser
|
|
8
|
+
|
|
9
|
+
include ParserHelper
|
|
10
|
+
|
|
11
|
+
def initialize(parser) = @parser = parser
|
|
12
|
+
|
|
13
|
+
def build(it)
|
|
14
|
+
last_it = it.matching_rparen
|
|
15
|
+
|
|
16
|
+
return nil unless it.lparen?
|
|
17
|
+
|
|
18
|
+
error("')' that should match '(' is not found", it) if last_it.nil?
|
|
19
|
+
|
|
20
|
+
it.next.skip_space
|
|
21
|
+
|
|
22
|
+
name = read_name(it)
|
|
23
|
+
|
|
24
|
+
return nil if %w[%if %else %for %include].include?(name)
|
|
25
|
+
|
|
26
|
+
it.next.skip_space
|
|
27
|
+
|
|
28
|
+
shortcut_attrs = parser.attr_shortcut_reader.read(it) || {}
|
|
29
|
+
|
|
30
|
+
it.skip_space unless shortcut_attrs.empty?
|
|
31
|
+
|
|
32
|
+
block_attrs = parser.attr_block_reader.read(it) || {}
|
|
33
|
+
|
|
34
|
+
attrs = merge_attrs(shortcut_attrs, block_attrs)
|
|
35
|
+
|
|
36
|
+
it.next.skip_space unless block_attrs.empty?
|
|
37
|
+
|
|
38
|
+
children = if it.slash?
|
|
39
|
+
it.next.skip_space
|
|
40
|
+
create_pseude_child(it, last_it)
|
|
41
|
+
else
|
|
42
|
+
parser.read_children(it, last_it)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
Element.new(name, attrs, children)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def read_name(it)
|
|
49
|
+
error("unexpected character '#{it.val}'", it) unless it.symbol?
|
|
50
|
+
it.val
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def merge_attrs(first, second)
|
|
54
|
+
return first if second.empty?
|
|
55
|
+
return second if first.empty?
|
|
56
|
+
|
|
57
|
+
second.each do |name, node|
|
|
58
|
+
if name == :class
|
|
59
|
+
first[:class] = first.fetch(:class, []).concat(node)
|
|
60
|
+
else
|
|
61
|
+
first[name] = node
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
first
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def create_pseude_child(it, last_it)
|
|
69
|
+
sub_stream = TokenStream.from_iterators(it, last_it)
|
|
70
|
+
sub_stream.push_front(Token.new(type: :lparen, val: '(', line: -1, col: -1))
|
|
71
|
+
sub_stream.push_back(Token.new(type: :rparen, val: ')', line: -1, col: -1))
|
|
72
|
+
|
|
73
|
+
element = read_child_element(sub_stream.begin)
|
|
74
|
+
it.step(sub_stream.size - 2)
|
|
75
|
+
|
|
76
|
+
[element].compact
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def read_child_element(it) = parser.element_builder.build(it)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
require_relative 'parser_helper'
|
|
2
|
+
|
|
3
|
+
module Sade
|
|
4
|
+
class ElseBuilder
|
|
5
|
+
attr_reader :parser
|
|
6
|
+
|
|
7
|
+
include ParserHelper
|
|
8
|
+
|
|
9
|
+
def initialize(parser)
|
|
10
|
+
@parser = parser
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def build(it)
|
|
14
|
+
last_it = it.matching_rparen
|
|
15
|
+
|
|
16
|
+
return nil unless else_expr?(it)
|
|
17
|
+
error("')' that should match '(' is not found", it) if last_it.nil?
|
|
18
|
+
|
|
19
|
+
it.next.skip_space # ( とその後ろのスペースをスキップ
|
|
20
|
+
it.next.skip_space # %else とその後ろのスペースをスキップ
|
|
21
|
+
|
|
22
|
+
Else.new(parser.read_children(it, last_it))
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|