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,433 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "token"
|
|
4
|
+
require_relative "ast"
|
|
5
|
+
require_relative "errors"
|
|
6
|
+
require_relative "lexer"
|
|
7
|
+
|
|
8
|
+
module Gemite
|
|
9
|
+
# <?gemite ... ?> の中身や、式の断片(補間・@ifの条件式など)をASTへ変換する。
|
|
10
|
+
# 再帰下降パーサー。
|
|
11
|
+
class Parser
|
|
12
|
+
include AST
|
|
13
|
+
|
|
14
|
+
# ソース文字列全体を「文の並び」としてパースする(<?gemite ?>ブロックの中身用)
|
|
15
|
+
def self.parse_statements_from_source(source, line: 1)
|
|
16
|
+
tokens = Lexer.new(source, line: line).tokenize
|
|
17
|
+
new(tokens).parse_statements_until(nil)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# ソース文字列を単一の式としてパースする({expr}補間や@if条件式用)
|
|
21
|
+
def self.parse_expression_from_source(source, line: 1)
|
|
22
|
+
tokens = Lexer.new(source, line: line).tokenize
|
|
23
|
+
parser = new(tokens)
|
|
24
|
+
parser.send(:skip_newlines)
|
|
25
|
+
expr = parser.send(:parse_expression)
|
|
26
|
+
parser.send(:skip_newlines)
|
|
27
|
+
unless parser.send(:check, :EOF)
|
|
28
|
+
tok = parser.send(:current)
|
|
29
|
+
raise Gemite::SyntaxError.new("式の後に余分なトークンがあります (#{tok.type})", line: tok.line)
|
|
30
|
+
end
|
|
31
|
+
expr
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# "var in iterable" 形式(@forの節)をパースする
|
|
35
|
+
def self.parse_for_clause_from_source(source, line: 1)
|
|
36
|
+
tokens = Lexer.new(source, line: line).tokenize
|
|
37
|
+
new(tokens).parse_for_clause
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def initialize(tokens)
|
|
41
|
+
@tokens = tokens
|
|
42
|
+
@pos = 0
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def parse_for_clause
|
|
46
|
+
name_tok = expect(:IDENT)
|
|
47
|
+
expect(:IN)
|
|
48
|
+
iterable = parse_expression
|
|
49
|
+
[name_tok.value, iterable]
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# stop_types: nilならEOFまで。配列ならそのトークン型に出会うまで(消費はしない)。
|
|
53
|
+
def parse_statements_until(stop_types)
|
|
54
|
+
stmts = []
|
|
55
|
+
skip_newlines
|
|
56
|
+
loop do
|
|
57
|
+
break if check(:EOF)
|
|
58
|
+
break if stop_types && stop_types.include?(current.type)
|
|
59
|
+
|
|
60
|
+
stmts << parse_statement
|
|
61
|
+
skip_newlines
|
|
62
|
+
end
|
|
63
|
+
stmts
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def current
|
|
69
|
+
@tokens[@pos]
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def peek(offset = 1)
|
|
73
|
+
idx = @pos + offset
|
|
74
|
+
idx < @tokens.length ? @tokens[idx] : @tokens.last
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def advance
|
|
78
|
+
tok = current
|
|
79
|
+
@pos += 1 unless tok.type == :EOF
|
|
80
|
+
tok
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def check(type)
|
|
84
|
+
current.type == type
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def accept(type)
|
|
88
|
+
check(type) ? advance : nil
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def expect(type)
|
|
92
|
+
unless check(type)
|
|
93
|
+
raise Gemite::SyntaxError.new(
|
|
94
|
+
"#{type} を期待しましたが #{current.type}(#{current.value.inspect}) でした",
|
|
95
|
+
line: current.line
|
|
96
|
+
)
|
|
97
|
+
end
|
|
98
|
+
advance
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def skip_newlines
|
|
102
|
+
advance while check(:NEWLINE)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# ============ 文 ============
|
|
106
|
+
|
|
107
|
+
def parse_statement
|
|
108
|
+
case current.type
|
|
109
|
+
when :DEF then parse_function_def
|
|
110
|
+
when :RETURN then parse_return
|
|
111
|
+
when :AT_IF then parse_if_stmt
|
|
112
|
+
when :AT_FOR then parse_for_stmt
|
|
113
|
+
when :BEGIN then parse_begin_catch
|
|
114
|
+
when :IDENT
|
|
115
|
+
peek.type == :ASSIGN ? parse_assignment : ExprStmt.new(parse_expression)
|
|
116
|
+
else
|
|
117
|
+
ExprStmt.new(parse_expression)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def parse_assignment
|
|
122
|
+
name_tok = expect(:IDENT)
|
|
123
|
+
expect(:ASSIGN)
|
|
124
|
+
skip_newlines
|
|
125
|
+
value = parse_expression
|
|
126
|
+
Assignment.new(name_tok.value, value)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def parse_function_def
|
|
130
|
+
expect(:DEF)
|
|
131
|
+
name_tok = expect(:IDENT)
|
|
132
|
+
expect(:LPAREN)
|
|
133
|
+
params = []
|
|
134
|
+
skip_newlines
|
|
135
|
+
unless check(:RPAREN)
|
|
136
|
+
loop do
|
|
137
|
+
skip_newlines
|
|
138
|
+
params << expect(:IDENT).value
|
|
139
|
+
skip_newlines
|
|
140
|
+
break unless accept(:COMMA)
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
skip_newlines
|
|
144
|
+
expect(:RPAREN)
|
|
145
|
+
body = parse_statements_until(%i[END])
|
|
146
|
+
expect(:END)
|
|
147
|
+
FunctionDef.new(name_tok.value, params, body)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
RETURN_STOPPERS = %i[NEWLINE EOF END AT_ENDIF AT_ELSEIF AT_ELSE AT_END CATCH].freeze
|
|
151
|
+
|
|
152
|
+
def parse_return
|
|
153
|
+
expect(:RETURN)
|
|
154
|
+
if RETURN_STOPPERS.include?(current.type)
|
|
155
|
+
ReturnStmt.new(NilLiteral.new(nil))
|
|
156
|
+
else
|
|
157
|
+
ReturnStmt.new(parse_expression)
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def parse_if_stmt
|
|
162
|
+
branches = []
|
|
163
|
+
expect(:AT_IF)
|
|
164
|
+
cond = parse_expression
|
|
165
|
+
body = parse_statements_until(%i[AT_ELSEIF AT_ELSE AT_ENDIF])
|
|
166
|
+
branches << [cond, body]
|
|
167
|
+
|
|
168
|
+
while check(:AT_ELSEIF)
|
|
169
|
+
advance
|
|
170
|
+
c = parse_expression
|
|
171
|
+
b = parse_statements_until(%i[AT_ELSEIF AT_ELSE AT_ENDIF])
|
|
172
|
+
branches << [c, b]
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
else_body = nil
|
|
176
|
+
if accept(:AT_ELSE)
|
|
177
|
+
else_body = parse_statements_until(%i[AT_ENDIF])
|
|
178
|
+
end
|
|
179
|
+
expect(:AT_ENDIF)
|
|
180
|
+
IfStmt.new(branches, else_body)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def parse_for_stmt
|
|
184
|
+
expect(:AT_FOR)
|
|
185
|
+
name_tok = expect(:IDENT)
|
|
186
|
+
expect(:IN)
|
|
187
|
+
iterable = parse_expression
|
|
188
|
+
body = parse_statements_until(%i[AT_END])
|
|
189
|
+
expect(:AT_END)
|
|
190
|
+
ForStmt.new(name_tok.value, iterable, body)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def parse_begin_catch
|
|
194
|
+
expect(:BEGIN)
|
|
195
|
+
begin_body = parse_statements_until(%i[CATCH])
|
|
196
|
+
expect(:CATCH)
|
|
197
|
+
err_tok = expect(:IDENT)
|
|
198
|
+
catch_body = parse_statements_until(%i[END])
|
|
199
|
+
expect(:END)
|
|
200
|
+
BeginCatchStmt.new(begin_body, err_tok.value, catch_body)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# ============ 式(優先順位の低い順) ============
|
|
204
|
+
|
|
205
|
+
def parse_expression
|
|
206
|
+
parse_or
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def parse_or
|
|
210
|
+
left = parse_and
|
|
211
|
+
while check(:OR)
|
|
212
|
+
advance
|
|
213
|
+
left = LogicalOp.new(:or, left, parse_and)
|
|
214
|
+
end
|
|
215
|
+
left
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def parse_and
|
|
219
|
+
left = parse_equality
|
|
220
|
+
while check(:AND)
|
|
221
|
+
advance
|
|
222
|
+
left = LogicalOp.new(:and, left, parse_equality)
|
|
223
|
+
end
|
|
224
|
+
left
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
EQUALITY_OPS = { EQ: :==, NEQ: :!= }.freeze
|
|
228
|
+
|
|
229
|
+
def parse_equality
|
|
230
|
+
left = parse_comparison
|
|
231
|
+
while EQUALITY_OPS.key?(current.type)
|
|
232
|
+
op = EQUALITY_OPS[advance.type]
|
|
233
|
+
left = BinaryOp.new(op, left, parse_comparison)
|
|
234
|
+
end
|
|
235
|
+
left
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
COMPARISON_OPS = { LT: :<, LTE: :<=, GT: :>, GTE: :>= }.freeze
|
|
239
|
+
|
|
240
|
+
def parse_comparison
|
|
241
|
+
left = parse_additive
|
|
242
|
+
while COMPARISON_OPS.key?(current.type)
|
|
243
|
+
op = COMPARISON_OPS[advance.type]
|
|
244
|
+
left = BinaryOp.new(op, left, parse_additive)
|
|
245
|
+
end
|
|
246
|
+
left
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
ADDITIVE_OPS = { PLUS: :+, MINUS: :- }.freeze
|
|
250
|
+
|
|
251
|
+
def parse_additive
|
|
252
|
+
left = parse_multiplicative
|
|
253
|
+
while ADDITIVE_OPS.key?(current.type)
|
|
254
|
+
op = ADDITIVE_OPS[advance.type]
|
|
255
|
+
left = BinaryOp.new(op, left, parse_multiplicative)
|
|
256
|
+
end
|
|
257
|
+
left
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
MULT_OPS = { STAR: :*, SLASH: :/, PERCENT: :% }.freeze
|
|
261
|
+
|
|
262
|
+
def parse_multiplicative
|
|
263
|
+
left = parse_unary
|
|
264
|
+
while MULT_OPS.key?(current.type)
|
|
265
|
+
op = MULT_OPS[advance.type]
|
|
266
|
+
left = BinaryOp.new(op, left, parse_unary)
|
|
267
|
+
end
|
|
268
|
+
left
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def parse_unary
|
|
272
|
+
if check(:MINUS)
|
|
273
|
+
advance
|
|
274
|
+
return UnaryOp.new(:-@, parse_unary)
|
|
275
|
+
end
|
|
276
|
+
if check(:NOT)
|
|
277
|
+
advance
|
|
278
|
+
return UnaryOp.new(:not, parse_unary)
|
|
279
|
+
end
|
|
280
|
+
parse_postfix
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def parse_postfix
|
|
284
|
+
expr = parse_primary
|
|
285
|
+
loop do
|
|
286
|
+
case current.type
|
|
287
|
+
when :DOT
|
|
288
|
+
advance
|
|
289
|
+
name_tok = expect(:IDENT)
|
|
290
|
+
if check(:LPAREN)
|
|
291
|
+
advance
|
|
292
|
+
args = parse_call_args
|
|
293
|
+
expect(:RPAREN)
|
|
294
|
+
expr = MethodCallExpr.new(expr, name_tok.value, args)
|
|
295
|
+
else
|
|
296
|
+
expr = MemberAccess.new(expr, name_tok.value)
|
|
297
|
+
end
|
|
298
|
+
when :LBRACKET
|
|
299
|
+
advance
|
|
300
|
+
skip_newlines
|
|
301
|
+
idx = parse_expression
|
|
302
|
+
skip_newlines
|
|
303
|
+
expect(:RBRACKET)
|
|
304
|
+
expr = IndexAccess.new(expr, idx)
|
|
305
|
+
else
|
|
306
|
+
break
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
expr
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def parse_primary
|
|
313
|
+
tok = current
|
|
314
|
+
case tok.type
|
|
315
|
+
when :INT
|
|
316
|
+
advance
|
|
317
|
+
IntLiteral.new(tok.value)
|
|
318
|
+
when :FLOAT
|
|
319
|
+
advance
|
|
320
|
+
FloatLiteral.new(tok.value)
|
|
321
|
+
when :STRING
|
|
322
|
+
advance
|
|
323
|
+
StringLiteral.new(build_string_parts(tok.value, tok.line))
|
|
324
|
+
when :TRUE
|
|
325
|
+
advance
|
|
326
|
+
BoolLiteral.new(true)
|
|
327
|
+
when :FALSE
|
|
328
|
+
advance
|
|
329
|
+
BoolLiteral.new(false)
|
|
330
|
+
when :NIL
|
|
331
|
+
advance
|
|
332
|
+
NilLiteral.new(nil)
|
|
333
|
+
when :IDENT
|
|
334
|
+
advance
|
|
335
|
+
if check(:LPAREN)
|
|
336
|
+
advance
|
|
337
|
+
args = parse_call_args
|
|
338
|
+
expect(:RPAREN)
|
|
339
|
+
CallExpr.new(tok.value, args)
|
|
340
|
+
else
|
|
341
|
+
Identifier.new(tok.value)
|
|
342
|
+
end
|
|
343
|
+
when :LPAREN
|
|
344
|
+
advance
|
|
345
|
+
skip_newlines
|
|
346
|
+
expr = parse_expression
|
|
347
|
+
skip_newlines
|
|
348
|
+
expect(:RPAREN)
|
|
349
|
+
expr
|
|
350
|
+
when :LBRACKET
|
|
351
|
+
parse_array_literal
|
|
352
|
+
when :LBRACE
|
|
353
|
+
parse_hash_literal
|
|
354
|
+
else
|
|
355
|
+
raise Gemite::SyntaxError.new("式が必要です ('#{tok.type}' は使用できません)", line: tok.line)
|
|
356
|
+
end
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
def parse_array_literal
|
|
360
|
+
expect(:LBRACKET)
|
|
361
|
+
elements = []
|
|
362
|
+
skip_newlines
|
|
363
|
+
unless check(:RBRACKET)
|
|
364
|
+
loop do
|
|
365
|
+
skip_newlines
|
|
366
|
+
elements << parse_expression
|
|
367
|
+
skip_newlines
|
|
368
|
+
break unless accept(:COMMA)
|
|
369
|
+
end
|
|
370
|
+
end
|
|
371
|
+
skip_newlines
|
|
372
|
+
expect(:RBRACKET)
|
|
373
|
+
ArrayLiteral.new(elements)
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
def parse_hash_literal
|
|
377
|
+
expect(:LBRACE)
|
|
378
|
+
pairs = []
|
|
379
|
+
skip_newlines
|
|
380
|
+
unless check(:RBRACE)
|
|
381
|
+
loop do
|
|
382
|
+
skip_newlines
|
|
383
|
+
key_tok = expect(:IDENT)
|
|
384
|
+
expect(:COLON)
|
|
385
|
+
skip_newlines
|
|
386
|
+
value = parse_expression
|
|
387
|
+
pairs << [key_tok.value.to_sym, value]
|
|
388
|
+
skip_newlines
|
|
389
|
+
break unless accept(:COMMA)
|
|
390
|
+
end
|
|
391
|
+
end
|
|
392
|
+
skip_newlines
|
|
393
|
+
expect(:RBRACE)
|
|
394
|
+
HashLiteral.new(pairs)
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
# 呼び出し引数。位置引数と "key: value" 形式(末尾に暗黙のハッシュとしてまとめる)の
|
|
398
|
+
# 両方に対応する。 db.select("users", where: {age: 15}) のような呼び出しのため。
|
|
399
|
+
def parse_call_args
|
|
400
|
+
positional = []
|
|
401
|
+
keyword_pairs = []
|
|
402
|
+
skip_newlines
|
|
403
|
+
unless check(:RPAREN)
|
|
404
|
+
loop do
|
|
405
|
+
skip_newlines
|
|
406
|
+
if check(:IDENT) && peek.type == :COLON
|
|
407
|
+
key_tok = advance
|
|
408
|
+
advance # COLON
|
|
409
|
+
skip_newlines
|
|
410
|
+
keyword_pairs << [key_tok.value.to_sym, parse_expression]
|
|
411
|
+
else
|
|
412
|
+
positional << parse_expression
|
|
413
|
+
end
|
|
414
|
+
skip_newlines
|
|
415
|
+
break unless accept(:COMMA)
|
|
416
|
+
end
|
|
417
|
+
end
|
|
418
|
+
skip_newlines
|
|
419
|
+
positional << HashLiteral.new(keyword_pairs) unless keyword_pairs.empty?
|
|
420
|
+
positional
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
def build_string_parts(raw_parts, line)
|
|
424
|
+
raw_parts.map do |(kind, val)|
|
|
425
|
+
if kind == :str
|
|
426
|
+
[:str, val]
|
|
427
|
+
else
|
|
428
|
+
[:expr, Parser.parse_expression_from_source(val, line: line)]
|
|
429
|
+
end
|
|
430
|
+
end
|
|
431
|
+
end
|
|
432
|
+
end
|
|
433
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gemite
|
|
4
|
+
# 生のHTTPリクエストをパースした結果を保持する。
|
|
5
|
+
class Request
|
|
6
|
+
attr_reader :method, :path, :query_params, :headers, :body, :form_params
|
|
7
|
+
|
|
8
|
+
def initialize(method:, path:, query_params:, headers:, body:)
|
|
9
|
+
@method = method
|
|
10
|
+
@path = path
|
|
11
|
+
@query_params = query_params
|
|
12
|
+
@headers = headers
|
|
13
|
+
@body = body
|
|
14
|
+
@form_params = parse_form_body
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# application/x-www-form-urlencoded のボディをパースする。
|
|
18
|
+
# multipart/form-data(ファイルアップロード)はv0.1では対象外。
|
|
19
|
+
def parse_form_body
|
|
20
|
+
return {} if body.nil? || body.empty?
|
|
21
|
+
|
|
22
|
+
content_type = headers["content-type"] || ""
|
|
23
|
+
return {} unless content_type.include?("application/x-www-form-urlencoded") || content_type.empty?
|
|
24
|
+
|
|
25
|
+
parse_www_form(body)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.parse_www_form(str)
|
|
29
|
+
result = {}
|
|
30
|
+
str.split("&").each do |pair|
|
|
31
|
+
next if pair.empty?
|
|
32
|
+
|
|
33
|
+
k, v = pair.split("=", 2)
|
|
34
|
+
result[uri_decode(k)] = uri_decode(v || "")
|
|
35
|
+
end
|
|
36
|
+
result
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.uri_decode(str)
|
|
40
|
+
str.to_s.tr("+", " ").gsub(/%([0-9A-Fa-f]{2})/) { [Regexp.last_match(1)].pack("H2").force_encoding("BINARY") }
|
|
41
|
+
.force_encoding("BINARY").force_encoding("UTF-8")
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def parse_www_form(str)
|
|
45
|
+
self.class.parse_www_form(str)
|
|
46
|
+
end
|
|
47
|
+
private :parse_www_form
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gemite
|
|
4
|
+
# リクエストパスから .gmt ファイル、または public/ 配下の静的ファイルを解決する。
|
|
5
|
+
#
|
|
6
|
+
# 仕様4: ファイルはURLへ直接マッピングされる(index.gmt -> /, login.gmt -> /login 等)。
|
|
7
|
+
# ディレクトリ構成は自由なので、サーバ起動時のカレントディレクトリを
|
|
8
|
+
# プロジェクトルートとして扱い、リクエストパスをそのまま相対パスとして解決する。
|
|
9
|
+
class Router
|
|
10
|
+
EXTENSION = ".gmt"
|
|
11
|
+
STATIC_DIR = "public"
|
|
12
|
+
|
|
13
|
+
MIME_TYPES = {
|
|
14
|
+
".html" => "text/html; charset=utf-8",
|
|
15
|
+
".htm" => "text/html; charset=utf-8",
|
|
16
|
+
".css" => "text/css; charset=utf-8",
|
|
17
|
+
".js" => "text/javascript; charset=utf-8",
|
|
18
|
+
".mjs" => "text/javascript; charset=utf-8",
|
|
19
|
+
".json" => "application/json; charset=utf-8",
|
|
20
|
+
".png" => "image/png",
|
|
21
|
+
".jpg" => "image/jpeg",
|
|
22
|
+
".jpeg" => "image/jpeg",
|
|
23
|
+
".gif" => "image/gif",
|
|
24
|
+
".svg" => "image/svg+xml",
|
|
25
|
+
".ico" => "image/x-icon",
|
|
26
|
+
".webp" => "image/webp",
|
|
27
|
+
".woff" => "font/woff",
|
|
28
|
+
".woff2" => "font/woff2",
|
|
29
|
+
".txt" => "text/plain; charset=utf-8",
|
|
30
|
+
".xml" => "application/xml; charset=utf-8",
|
|
31
|
+
".pdf" => "application/pdf"
|
|
32
|
+
}.freeze
|
|
33
|
+
|
|
34
|
+
def initialize(root)
|
|
35
|
+
@root = File.expand_path(root)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
Result = Struct.new(:type, :path) # type: :gemite | :static | :not_found
|
|
39
|
+
|
|
40
|
+
def resolve(request_path)
|
|
41
|
+
clean = normalize(request_path)
|
|
42
|
+
return Result.new(:not_found, nil) unless clean
|
|
43
|
+
|
|
44
|
+
gemite_path = find_gemite_file(clean)
|
|
45
|
+
return Result.new(:gemite, gemite_path) if gemite_path
|
|
46
|
+
|
|
47
|
+
static_path = find_static_file(clean)
|
|
48
|
+
return Result.new(:static, static_path) if static_path
|
|
49
|
+
|
|
50
|
+
Result.new(:not_found, nil)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def mime_type_for(path)
|
|
54
|
+
MIME_TYPES[File.extname(path).downcase] || "application/octet-stream"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
# ".." 等によるルート外アクセスを防ぐための正規化。
|
|
60
|
+
def normalize(request_path)
|
|
61
|
+
path = request_path.split("?").first.to_s
|
|
62
|
+
path = "/" if path.empty?
|
|
63
|
+
segments = path.split("/").reject { |s| s.empty? || s == "." }
|
|
64
|
+
return nil if segments.include?("..")
|
|
65
|
+
|
|
66
|
+
"/" + segments.join("/")
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def find_gemite_file(clean_path)
|
|
70
|
+
rel = clean_path == "/" ? "index" : clean_path.sub(%r{\A/}, "")
|
|
71
|
+
direct = safe_join("#{rel}#{EXTENSION}")
|
|
72
|
+
return direct if direct && File.file?(direct)
|
|
73
|
+
|
|
74
|
+
index = safe_join(File.join(rel, "index#{EXTENSION}"))
|
|
75
|
+
return index if index && File.file?(index)
|
|
76
|
+
|
|
77
|
+
nil
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def find_static_file(clean_path)
|
|
81
|
+
rel = clean_path.sub(%r{\A/}, "")
|
|
82
|
+
return nil if rel.empty?
|
|
83
|
+
|
|
84
|
+
candidate = safe_join(File.join(STATIC_DIR, rel))
|
|
85
|
+
return candidate if candidate && File.file?(candidate)
|
|
86
|
+
|
|
87
|
+
nil
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# @rootの外に出ないことを保証した上でパスを結合する。範囲外なら nil を返す。
|
|
91
|
+
def safe_join(rel)
|
|
92
|
+
full = File.expand_path(File.join(@root, rel))
|
|
93
|
+
return nil unless full == @root || full.start_with?(@root + File::SEPARATOR)
|
|
94
|
+
|
|
95
|
+
full
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|