gemite 0.1.0
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 +21 -0
- data/README.md +519 -0
- data/examples/comments/README.md +13 -0
- data/examples/comments/index.gmt +65 -0
- data/examples/comments/public/css/style.css +44 -0
- data/exe/gemite +6 -0
- data/lib/gemite/ast.rb +42 -0
- data/lib/gemite/cli.rb +81 -0
- data/lib/gemite/database.rb +101 -0
- data/lib/gemite/environment.rb +73 -0
- data/lib/gemite/errors.rb +39 -0
- data/lib/gemite/function.rb +21 -0
- data/lib/gemite/http_server.rb +181 -0
- data/lib/gemite/interpreter.rb +435 -0
- data/lib/gemite/lexer.rb +289 -0
- data/lib/gemite/parser.rb +433 -0
- data/lib/gemite/request.rb +49 -0
- data/lib/gemite/router.rb +98 -0
- data/lib/gemite/sqlite3_ffi.rb +223 -0
- data/lib/gemite/template_parser.rb +305 -0
- data/lib/gemite/token.rb +11 -0
- data/lib/gemite/version.rb +5 -0
- data/lib/gemite.rb +21 -0
- metadata +69 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fiddle"
|
|
4
|
+
require "fiddle/import"
|
|
5
|
+
require_relative "errors"
|
|
6
|
+
|
|
7
|
+
module Gemite
|
|
8
|
+
# libsqlite3 (システムに標準で入っている共有ライブラリ) を Fiddle 経由で直接呼び出す
|
|
9
|
+
# 低レベル層。sqlite3 gem のようなネイティブ拡張のビルド(コンパイラやヘッダが必要)を
|
|
10
|
+
# 一切必要とせず、`gem install gemite` が即座に完了することを狙う。
|
|
11
|
+
module SQLite3FFI
|
|
12
|
+
SQLITE_OK = 0
|
|
13
|
+
SQLITE_ROW = 100
|
|
14
|
+
SQLITE_DONE = 101
|
|
15
|
+
SQLITE_TRANSIENT = -1
|
|
16
|
+
|
|
17
|
+
SQLITE_INTEGER = 1
|
|
18
|
+
SQLITE_FLOAT = 2
|
|
19
|
+
SQLITE_TEXT = 3
|
|
20
|
+
SQLITE_BLOB = 4
|
|
21
|
+
SQLITE_NULL = 5
|
|
22
|
+
|
|
23
|
+
# OS ごとの共有ライブラリ名の候補。最初に見つかったものを使う。
|
|
24
|
+
CANDIDATE_LIBS = [
|
|
25
|
+
"libsqlite3.so.0",
|
|
26
|
+
"libsqlite3.so",
|
|
27
|
+
"sqlite3.so",
|
|
28
|
+
"libsqlite3.dylib",
|
|
29
|
+
"sqlite3.dll"
|
|
30
|
+
].freeze
|
|
31
|
+
|
|
32
|
+
module Lib
|
|
33
|
+
extend Fiddle::Importer
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
class LoadError < StandardError; end
|
|
37
|
+
|
|
38
|
+
def self.ensure_loaded!
|
|
39
|
+
return if @loaded
|
|
40
|
+
|
|
41
|
+
last_error = nil
|
|
42
|
+
CANDIDATE_LIBS.each do |name|
|
|
43
|
+
begin
|
|
44
|
+
Lib.dlload(name)
|
|
45
|
+
@loaded = name
|
|
46
|
+
break
|
|
47
|
+
rescue Fiddle::DLError => e
|
|
48
|
+
last_error = e
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
unless @loaded
|
|
53
|
+
raise LoadError,
|
|
54
|
+
"libsqlite3 が見つかりませんでした。システムに SQLite3 のランタイムライブラリ " \
|
|
55
|
+
"(libsqlite3-0 等) がインストールされているか確認してください。(#{last_error&.message})"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
define_externs!
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def self.define_externs!
|
|
62
|
+
Lib.extern "int sqlite3_open(const char*, void**)"
|
|
63
|
+
Lib.extern "int sqlite3_close(void*)"
|
|
64
|
+
Lib.extern "int sqlite3_prepare_v2(void*, const char*, int, void**, void**)"
|
|
65
|
+
Lib.extern "int sqlite3_step(void*)"
|
|
66
|
+
Lib.extern "int sqlite3_finalize(void*)"
|
|
67
|
+
Lib.extern "int sqlite3_bind_text(void*, int, const char*, int, void*)"
|
|
68
|
+
Lib.extern "int sqlite3_bind_int64(void*, int, long long)"
|
|
69
|
+
Lib.extern "int sqlite3_bind_double(void*, int, double)"
|
|
70
|
+
Lib.extern "int sqlite3_bind_null(void*, int)"
|
|
71
|
+
Lib.extern "int sqlite3_column_count(void*)"
|
|
72
|
+
Lib.extern "const char* sqlite3_column_name(void*, int)"
|
|
73
|
+
Lib.extern "int sqlite3_column_type(void*, int)"
|
|
74
|
+
Lib.extern "long long sqlite3_column_int64(void*, int)"
|
|
75
|
+
Lib.extern "double sqlite3_column_double(void*, int)"
|
|
76
|
+
Lib.extern "const char* sqlite3_column_text(void*, int)"
|
|
77
|
+
Lib.extern "const char* sqlite3_errmsg(void*)"
|
|
78
|
+
Lib.extern "long long sqlite3_last_insert_rowid(void*)"
|
|
79
|
+
Lib.extern "int sqlite3_changes(void*)"
|
|
80
|
+
end
|
|
81
|
+
private_class_method :define_externs!
|
|
82
|
+
|
|
83
|
+
# sqlite3* ハンドルと、その上での操作をまとめる薄いラッパー。
|
|
84
|
+
class Handle
|
|
85
|
+
def initialize(path)
|
|
86
|
+
SQLite3FFI.ensure_loaded!
|
|
87
|
+
@path = path
|
|
88
|
+
ptr = Fiddle::Pointer.malloc(Fiddle::SIZEOF_VOIDP)
|
|
89
|
+
rc = Lib.sqlite3_open(path, ptr)
|
|
90
|
+
@db = ptr.ptr
|
|
91
|
+
raise Gemite::RuntimeError, "データベース '#{path}' を開けませんでした" unless rc == SQLITE_OK
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# 生のSQLを実行する(SELECT以外、あるいは戻り値を使わない用途)。
|
|
95
|
+
# ";" 区切りの複数文にも対応する。
|
|
96
|
+
def exec(sql)
|
|
97
|
+
remaining = sql
|
|
98
|
+
loop do
|
|
99
|
+
remaining = remaining.to_s
|
|
100
|
+
break if remaining.strip.empty?
|
|
101
|
+
|
|
102
|
+
stmt, tail = prepare_raw(remaining)
|
|
103
|
+
unless stmt
|
|
104
|
+
# このSQL断片に実行可能な文が含まれていなかった(空白・コメントのみ等)
|
|
105
|
+
break
|
|
106
|
+
end
|
|
107
|
+
step_all(stmt)
|
|
108
|
+
Lib.sqlite3_finalize(stmt)
|
|
109
|
+
remaining = tail
|
|
110
|
+
end
|
|
111
|
+
nil
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# SELECT文を実行し、行の配列(各行は列名Symbol => 値 のHash)を返す。
|
|
115
|
+
def query(sql, params = [])
|
|
116
|
+
stmt, = prepare_raw(sql)
|
|
117
|
+
raise Gemite::RuntimeError, "SQL準備に失敗しました: #{last_error}" unless stmt
|
|
118
|
+
|
|
119
|
+
bind_params(stmt, params)
|
|
120
|
+
rows = []
|
|
121
|
+
col_count = Lib.sqlite3_column_count(stmt)
|
|
122
|
+
col_names = Array.new(col_count) { |i| Lib.sqlite3_column_name(stmt, i).to_s.to_sym }
|
|
123
|
+
|
|
124
|
+
while Lib.sqlite3_step(stmt) == SQLITE_ROW
|
|
125
|
+
row = {}
|
|
126
|
+
col_count.times do |i|
|
|
127
|
+
row[col_names[i]] = read_column(stmt, i)
|
|
128
|
+
end
|
|
129
|
+
rows << row
|
|
130
|
+
end
|
|
131
|
+
Lib.sqlite3_finalize(stmt)
|
|
132
|
+
rows
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# INSERT/UPDATE/DELETE等、戻り値の行を必要としない文を実行する。
|
|
136
|
+
# {rowid:, changes:} を返す。
|
|
137
|
+
def execute(sql, params = [])
|
|
138
|
+
stmt, = prepare_raw(sql)
|
|
139
|
+
raise Gemite::RuntimeError, "SQL準備に失敗しました: #{last_error}" unless stmt
|
|
140
|
+
|
|
141
|
+
bind_params(stmt, params)
|
|
142
|
+
rc = Lib.sqlite3_step(stmt)
|
|
143
|
+
Lib.sqlite3_finalize(stmt)
|
|
144
|
+
unless rc == SQLITE_DONE
|
|
145
|
+
raise Gemite::RuntimeError, "SQL実行に失敗しました: #{last_error}"
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
{ rowid: Lib.sqlite3_last_insert_rowid(@db), changes: Lib.sqlite3_changes(@db) }
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def close
|
|
152
|
+
Lib.sqlite3_close(@db) if @db
|
|
153
|
+
@db = nil
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
private
|
|
157
|
+
|
|
158
|
+
def last_error
|
|
159
|
+
Lib.sqlite3_errmsg(@db).to_s
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# 1文だけprepareする。戻り値は [stmt_ptr_or_nil, tail_sql_string]。
|
|
163
|
+
def prepare_raw(sql)
|
|
164
|
+
stmt_ptr = Fiddle::Pointer.malloc(Fiddle::SIZEOF_VOIDP)
|
|
165
|
+
tail_ptr = Fiddle::Pointer.malloc(Fiddle::SIZEOF_VOIDP)
|
|
166
|
+
sql_str = sql.to_s
|
|
167
|
+
rc = Lib.sqlite3_prepare_v2(@db, sql_str, sql_str.bytesize, stmt_ptr, tail_ptr)
|
|
168
|
+
raise Gemite::RuntimeError, "SQL構文エラー: #{last_error}" unless rc == SQLITE_OK
|
|
169
|
+
|
|
170
|
+
stmt = stmt_ptr.ptr
|
|
171
|
+
tail_addr = tail_ptr.ptr
|
|
172
|
+
tail = tail_addr.null? ? "" : tail_addr.to_s
|
|
173
|
+
[stmt.null? ? nil : stmt, tail]
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def step_all(stmt)
|
|
177
|
+
loop do
|
|
178
|
+
rc = Lib.sqlite3_step(stmt)
|
|
179
|
+
break if rc == SQLITE_DONE
|
|
180
|
+
break if rc == SQLITE_ROW # execで結果行が来ても単に読み捨てる
|
|
181
|
+
|
|
182
|
+
unless [SQLITE_DONE, SQLITE_ROW].include?(rc)
|
|
183
|
+
raise Gemite::RuntimeError, "SQL実行に失敗しました: #{last_error}"
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def bind_params(stmt, params)
|
|
189
|
+
params.each_with_index do |val, idx|
|
|
190
|
+
i = idx + 1
|
|
191
|
+
case val
|
|
192
|
+
when nil
|
|
193
|
+
Lib.sqlite3_bind_null(stmt, i)
|
|
194
|
+
when true
|
|
195
|
+
Lib.sqlite3_bind_int64(stmt, i, 1)
|
|
196
|
+
when false
|
|
197
|
+
Lib.sqlite3_bind_int64(stmt, i, 0)
|
|
198
|
+
when Integer
|
|
199
|
+
Lib.sqlite3_bind_int64(stmt, i, val)
|
|
200
|
+
when Float
|
|
201
|
+
Lib.sqlite3_bind_double(stmt, i, val)
|
|
202
|
+
else
|
|
203
|
+
s = val.to_s
|
|
204
|
+
bytes = s.b
|
|
205
|
+
Lib.sqlite3_bind_text(stmt, i, s, bytes.bytesize, SQLITE_TRANSIENT)
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def read_column(stmt, idx)
|
|
211
|
+
type = Lib.sqlite3_column_type(stmt, idx)
|
|
212
|
+
case type
|
|
213
|
+
when SQLITE_INTEGER then Lib.sqlite3_column_int64(stmt, idx)
|
|
214
|
+
when SQLITE_FLOAT then Lib.sqlite3_column_double(stmt, idx)
|
|
215
|
+
when SQLITE_TEXT
|
|
216
|
+
raw = Lib.sqlite3_column_text(stmt, idx)
|
|
217
|
+
raw.nil? ? nil : raw.to_s.dup.force_encoding("UTF-8")
|
|
218
|
+
else nil # SQLITE_NULL, SQLITE_BLOB(未対応)
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
end
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "ast"
|
|
4
|
+
require_relative "parser"
|
|
5
|
+
require_relative "errors"
|
|
6
|
+
|
|
7
|
+
module Gemite
|
|
8
|
+
# .tz ファイルの生テキストを走査し、以下のノード列(木)を組み立てる:
|
|
9
|
+
# TplText - そのまま出力する生HTML
|
|
10
|
+
# TplExpr - { expr } 補間。式の評価結果を文字列化して出力する
|
|
11
|
+
# TplCode - <?gemite ... ?> ブロック。中身は文として実行するだけで出力はしない
|
|
12
|
+
# TplIf - @if/@elseif/@else/@endif (HTML側)。分岐に応じて子ノードを出力する
|
|
13
|
+
# TplFor - @for x in xs ... @end (HTML側)。反復して子ノードを出力する
|
|
14
|
+
#
|
|
15
|
+
# <?gemite ?> の中身、および @if/@for がコードブロック内で使われた場合は、
|
|
16
|
+
# Parser(文パーサー)側に委譲する。@if/@endif, @for/@end はテンプレート層・
|
|
17
|
+
# コード層の両方に現れうるが、defやbeginの素のendはコード層にしか現れない。
|
|
18
|
+
class TemplateParser
|
|
19
|
+
include AST
|
|
20
|
+
|
|
21
|
+
# コードブロックの開始タグ。文字数に依存する処理はすべてこの定数の
|
|
22
|
+
# .length から動的に算出し、名称変更があってもズレないようにする。
|
|
23
|
+
CODE_TAG_OPEN = "<?gemite"
|
|
24
|
+
CODE_TAG_CLOSE = "?>"
|
|
25
|
+
|
|
26
|
+
# @if/@elseif/@for は条件式・イテラブル式(行の残り)を伴う。
|
|
27
|
+
# @else/@endif/@end は節を取らないので、キーワード自体だけを消費する
|
|
28
|
+
# (でないと同じ行に続くHTMLや<?gemite ?>を誤って飲み込んでしまう)。
|
|
29
|
+
# 行頭(前後の空白のみ許容)に限定することで、HTML本文中の"@"(メールアドレス等)との衝突も避ける。
|
|
30
|
+
DIRECTIVE_WITH_CLAUSE = /\A[ \t]*@(if|elseif|for)\b[ \t]*(.*)\z/
|
|
31
|
+
DIRECTIVE_BARE = /\A[ \t]*@(else|endif|end)\b[ \t]*/
|
|
32
|
+
|
|
33
|
+
def self.parse(source)
|
|
34
|
+
new(source).parse_nodes(nil)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def initialize(source)
|
|
38
|
+
@src = source
|
|
39
|
+
@pos = 0
|
|
40
|
+
@len = source.length
|
|
41
|
+
@line = 1
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# stop_directives: nilならEOFまで。配列ならそのディレクティブ名(文字列)に
|
|
45
|
+
# 出会うまで(消費はしない。呼び出し元がexpectする)。
|
|
46
|
+
def parse_nodes(stop_directives)
|
|
47
|
+
nodes = []
|
|
48
|
+
text_buf = +""
|
|
49
|
+
text_start_line = @line
|
|
50
|
+
|
|
51
|
+
flush = lambda do
|
|
52
|
+
unless text_buf.empty?
|
|
53
|
+
nodes << TplText.new(text_buf)
|
|
54
|
+
text_buf = +""
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
until eof?
|
|
59
|
+
# <?gemite ... ?> コードブロック
|
|
60
|
+
if looking_at?(CODE_TAG_OPEN)
|
|
61
|
+
flush.call
|
|
62
|
+
nodes << parse_code_block
|
|
63
|
+
text_start_line = @line
|
|
64
|
+
next
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# { expr } 補間 (失敗したら生テキストとしてフォールバック)
|
|
68
|
+
if peek_char == "{"
|
|
69
|
+
expr_node = try_parse_interpolation
|
|
70
|
+
if expr_node
|
|
71
|
+
flush.call
|
|
72
|
+
nodes << expr_node
|
|
73
|
+
text_start_line = @line
|
|
74
|
+
next
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# 行頭ディレクティブ (@if/@elseif/@else/@endif/@for/@end)
|
|
79
|
+
if at_line_start?
|
|
80
|
+
directive, clause, consumed_len = peek_directive_at_current_line
|
|
81
|
+
if directive
|
|
82
|
+
if stop_directives && stop_directives.include?(directive)
|
|
83
|
+
# ここでは消費しない。呼び出し元が consume_matched_directive で読み進める。
|
|
84
|
+
flush.call
|
|
85
|
+
return nodes
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
consume_chars(consumed_len)
|
|
89
|
+
|
|
90
|
+
case directive
|
|
91
|
+
when "if"
|
|
92
|
+
flush.call
|
|
93
|
+
nodes << parse_template_if(clause)
|
|
94
|
+
text_start_line = @line
|
|
95
|
+
next
|
|
96
|
+
when "for"
|
|
97
|
+
flush.call
|
|
98
|
+
nodes << parse_template_for(clause)
|
|
99
|
+
text_start_line = @line
|
|
100
|
+
next
|
|
101
|
+
when "elseif", "else", "endif", "end"
|
|
102
|
+
# 対応する開始ディレクティブなしに出現した閉じ/中間ディレクティブ
|
|
103
|
+
raise Gemite::SyntaxError.new("対応する開始タグのない '@#{directive}' です", line: @line)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
ch = advance_char
|
|
109
|
+
text_buf << ch
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
if stop_directives
|
|
113
|
+
raise Gemite::SyntaxError.new(
|
|
114
|
+
"'#{stop_directives.map { |d| "@#{d}" }.join("/")}' が見つからないまま終端しました",
|
|
115
|
+
line: text_start_line
|
|
116
|
+
)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
flush.call
|
|
120
|
+
nodes
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
private
|
|
124
|
+
|
|
125
|
+
def eof?
|
|
126
|
+
@pos >= @len
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def peek_char(offset = 0)
|
|
130
|
+
idx = @pos + offset
|
|
131
|
+
idx < @len ? @src[idx] : nil
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def advance_char
|
|
135
|
+
ch = @src[@pos]
|
|
136
|
+
@pos += 1
|
|
137
|
+
@line += 1 if ch == "\n"
|
|
138
|
+
ch
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def looking_at?(str)
|
|
142
|
+
@src[@pos, str.length] == str
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# 現在位置が「行頭」(直前が改行かファイル先頭)かどうか
|
|
146
|
+
def at_line_start?
|
|
147
|
+
@pos.zero? || @src[@pos - 1] == "\n"
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# 現在行(行頭からの残り)が @if/@elseif/... のディレクティブ行かどうかを、
|
|
151
|
+
# 文字を消費せずに調べる。マッチした場合、ディレクティブ名・節の文字列
|
|
152
|
+
# (条件式など。@else/@endif/@endの場合は空文字列)・マッチした長さ(文字数)を返す。
|
|
153
|
+
def peek_directive_at_current_line
|
|
154
|
+
line_end = @src.index("\n", @pos) || @len
|
|
155
|
+
line_text = @src[@pos...line_end]
|
|
156
|
+
|
|
157
|
+
m = DIRECTIVE_WITH_CLAUSE.match(line_text)
|
|
158
|
+
return [m[1], m[2].to_s.strip, m[0].length] if m
|
|
159
|
+
|
|
160
|
+
m = DIRECTIVE_BARE.match(line_text)
|
|
161
|
+
return [m[1], "", m[0].length] if m
|
|
162
|
+
|
|
163
|
+
[nil, nil, 0]
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def consume_chars(n)
|
|
167
|
+
n.times { advance_char }
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def parse_code_block
|
|
171
|
+
start_line = @line
|
|
172
|
+
consume_chars(CODE_TAG_OPEN.length)
|
|
173
|
+
code_start_pos = @pos
|
|
174
|
+
code_start_line = @line
|
|
175
|
+
close_idx = @src.index(CODE_TAG_CLOSE, @pos)
|
|
176
|
+
unless close_idx
|
|
177
|
+
raise Gemite::SyntaxError.new("#{CODE_TAG_OPEN} ブロックが '#{CODE_TAG_CLOSE}' で閉じられていません", line: start_line)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
code_src = @src[code_start_pos...close_idx]
|
|
181
|
+
advance_char while @pos < close_idx
|
|
182
|
+
consume_chars(CODE_TAG_CLOSE.length)
|
|
183
|
+
|
|
184
|
+
statements = Gemite::Parser.parse_statements_from_source(code_src, line: code_start_line)
|
|
185
|
+
TplCode.new(statements)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# { ... } を式として解析する。対応する } を探すために深さと文字列リテラルを
|
|
189
|
+
# 追跡する。パースに失敗、または対応する } が見つからない場合は nil を返し、
|
|
190
|
+
# 呼び出し元は "{" を通常のテキスト文字として扱う(CSS等の { } との衝突回避)。
|
|
191
|
+
def try_parse_interpolation
|
|
192
|
+
saved_pos = @pos
|
|
193
|
+
saved_line = @line
|
|
194
|
+
|
|
195
|
+
advance_char # '{'
|
|
196
|
+
depth = 1
|
|
197
|
+
in_str = false
|
|
198
|
+
buf = +""
|
|
199
|
+
|
|
200
|
+
loop do
|
|
201
|
+
ch = peek_char
|
|
202
|
+
if ch.nil?
|
|
203
|
+
@pos = saved_pos
|
|
204
|
+
@line = saved_line
|
|
205
|
+
return nil
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
if in_str
|
|
209
|
+
buf << ch
|
|
210
|
+
if ch == "\\"
|
|
211
|
+
advance_char
|
|
212
|
+
buf << peek_char.to_s
|
|
213
|
+
advance_char
|
|
214
|
+
elsif ch == '"'
|
|
215
|
+
in_str = false
|
|
216
|
+
advance_char
|
|
217
|
+
else
|
|
218
|
+
advance_char
|
|
219
|
+
end
|
|
220
|
+
next
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
case ch
|
|
224
|
+
when '"'
|
|
225
|
+
in_str = true
|
|
226
|
+
buf << ch
|
|
227
|
+
advance_char
|
|
228
|
+
when "{"
|
|
229
|
+
depth += 1
|
|
230
|
+
buf << ch
|
|
231
|
+
advance_char
|
|
232
|
+
when "}"
|
|
233
|
+
depth -= 1
|
|
234
|
+
if depth.zero?
|
|
235
|
+
advance_char
|
|
236
|
+
break
|
|
237
|
+
else
|
|
238
|
+
buf << ch
|
|
239
|
+
advance_char
|
|
240
|
+
end
|
|
241
|
+
when "\n"
|
|
242
|
+
# 補間は基本1行で完結する想定。複数行にまたがる場合は生テキスト扱いにする
|
|
243
|
+
# (CSSブロック等をより安全側に倒すため)。
|
|
244
|
+
@pos = saved_pos
|
|
245
|
+
@line = saved_line
|
|
246
|
+
return nil
|
|
247
|
+
else
|
|
248
|
+
buf << ch
|
|
249
|
+
advance_char
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
begin
|
|
254
|
+
expr = Gemite::Parser.parse_expression_from_source(buf, line: saved_line)
|
|
255
|
+
TplExpr.new(expr)
|
|
256
|
+
rescue Gemite::SyntaxError
|
|
257
|
+
@pos = saved_pos
|
|
258
|
+
@line = saved_line
|
|
259
|
+
nil
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def parse_template_if(first_clause_src)
|
|
264
|
+
branches = []
|
|
265
|
+
cond = Gemite::Parser.parse_expression_from_source(first_clause_src, line: @line)
|
|
266
|
+
body = parse_nodes(%w[elseif else endif])
|
|
267
|
+
branches << [cond, body]
|
|
268
|
+
|
|
269
|
+
loop do
|
|
270
|
+
directive, clause = consume_matched_directive
|
|
271
|
+
case directive
|
|
272
|
+
when "elseif"
|
|
273
|
+
c = Gemite::Parser.parse_expression_from_source(clause, line: @line)
|
|
274
|
+
b = parse_nodes(%w[elseif else endif])
|
|
275
|
+
branches << [c, b]
|
|
276
|
+
when "else"
|
|
277
|
+
else_body = parse_nodes(%w[endif])
|
|
278
|
+
consume_matched_directive # endif
|
|
279
|
+
return TplIf.new(branches, else_body)
|
|
280
|
+
when "endif"
|
|
281
|
+
return TplIf.new(branches, nil)
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def parse_template_for(clause_src)
|
|
287
|
+
var_name, iterable = Gemite::Parser.parse_for_clause_from_source(clause_src, line: @line)
|
|
288
|
+
body = parse_nodes(%w[end])
|
|
289
|
+
consume_matched_directive # end
|
|
290
|
+
TplFor.new(var_name, iterable, body)
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
# parse_nodes が stop_directives に当たって(消費せずに)return した直後、
|
|
294
|
+
# その行頭ディレクティブを実際に読み進めるためのヘルパー。
|
|
295
|
+
def consume_matched_directive
|
|
296
|
+
raise Gemite::SyntaxError.new("内部エラー: ディレクティブ行が見つかりません", line: @line) unless at_line_start?
|
|
297
|
+
|
|
298
|
+
directive, clause, consumed_len = peek_directive_at_current_line
|
|
299
|
+
raise Gemite::SyntaxError.new("内部エラー: ディレクティブの再マッチに失敗しました", line: @line) unless directive
|
|
300
|
+
|
|
301
|
+
consume_chars(consumed_len)
|
|
302
|
+
[directive, clause]
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
end
|
data/lib/gemite/token.rb
ADDED
data/lib/gemite.rb
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "gemite/version"
|
|
4
|
+
require_relative "gemite/errors"
|
|
5
|
+
require_relative "gemite/token"
|
|
6
|
+
require_relative "gemite/lexer"
|
|
7
|
+
require_relative "gemite/ast"
|
|
8
|
+
require_relative "gemite/parser"
|
|
9
|
+
require_relative "gemite/environment"
|
|
10
|
+
require_relative "gemite/template_parser"
|
|
11
|
+
require_relative "gemite/function"
|
|
12
|
+
require_relative "gemite/sqlite3_ffi"
|
|
13
|
+
require_relative "gemite/database"
|
|
14
|
+
require_relative "gemite/interpreter"
|
|
15
|
+
require_relative "gemite/request"
|
|
16
|
+
require_relative "gemite/router"
|
|
17
|
+
require_relative "gemite/http_server"
|
|
18
|
+
require_relative "gemite/cli"
|
|
19
|
+
|
|
20
|
+
module Gemite
|
|
21
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: gemite
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Araki-Tomoya
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: |
|
|
13
|
+
Gemite is a lightweight HTML-first programming language inspired by PHP.
|
|
14
|
+
It allows developers to build dynamic web applications with minimal setup,
|
|
15
|
+
built-in routing based on file structure, and integrated SQLite support.
|
|
16
|
+
executables:
|
|
17
|
+
- gemite
|
|
18
|
+
extensions: []
|
|
19
|
+
extra_rdoc_files: []
|
|
20
|
+
files:
|
|
21
|
+
- LICENSE
|
|
22
|
+
- README.md
|
|
23
|
+
- examples/comments/README.md
|
|
24
|
+
- examples/comments/index.gmt
|
|
25
|
+
- examples/comments/public/css/style.css
|
|
26
|
+
- exe/gemite
|
|
27
|
+
- lib/gemite.rb
|
|
28
|
+
- lib/gemite/ast.rb
|
|
29
|
+
- lib/gemite/cli.rb
|
|
30
|
+
- lib/gemite/database.rb
|
|
31
|
+
- lib/gemite/environment.rb
|
|
32
|
+
- lib/gemite/errors.rb
|
|
33
|
+
- lib/gemite/function.rb
|
|
34
|
+
- lib/gemite/http_server.rb
|
|
35
|
+
- lib/gemite/interpreter.rb
|
|
36
|
+
- lib/gemite/lexer.rb
|
|
37
|
+
- lib/gemite/parser.rb
|
|
38
|
+
- lib/gemite/request.rb
|
|
39
|
+
- lib/gemite/router.rb
|
|
40
|
+
- lib/gemite/sqlite3_ffi.rb
|
|
41
|
+
- lib/gemite/template_parser.rb
|
|
42
|
+
- lib/gemite/token.rb
|
|
43
|
+
- lib/gemite/version.rb
|
|
44
|
+
homepage: https://github.com/SrinivasaRimskyYamatov/gemite
|
|
45
|
+
licenses:
|
|
46
|
+
- MIT
|
|
47
|
+
metadata:
|
|
48
|
+
homepage_uri: https://github.com/SrinivasaRimskyYamatov/gemite
|
|
49
|
+
source_code_uri: https://github.com/SrinivasaRimskyYamatov/gemite
|
|
50
|
+
bug_tracker_uri: https://github.com/SrinivasaRimskyYamatov/gemite/issues
|
|
51
|
+
changelog_uri: https://github.com/SrinivasaRimskyYamatov/gemite/releases
|
|
52
|
+
rdoc_options: []
|
|
53
|
+
require_paths:
|
|
54
|
+
- lib
|
|
55
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
56
|
+
requirements:
|
|
57
|
+
- - ">="
|
|
58
|
+
- !ruby/object:Gem::Version
|
|
59
|
+
version: '2.7'
|
|
60
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
61
|
+
requirements:
|
|
62
|
+
- - ">="
|
|
63
|
+
- !ruby/object:Gem::Version
|
|
64
|
+
version: '0'
|
|
65
|
+
requirements: []
|
|
66
|
+
rubygems_version: 4.0.14
|
|
67
|
+
specification_version: 4
|
|
68
|
+
summary: HTML-first language for small web applications
|
|
69
|
+
test_files: []
|