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
data/lib/gemite/cli.rb
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "version"
|
|
4
|
+
require_relative "http_server"
|
|
5
|
+
|
|
6
|
+
module Gemite
|
|
7
|
+
# `gemite` コマンドの実装。仕様3のコマンド体系:
|
|
8
|
+
# gemite 開発サーバ起動 (localhost:8000)
|
|
9
|
+
# gemite s 開発サーバ起動
|
|
10
|
+
# gemite s 3000 ポート指定
|
|
11
|
+
# gemite start "gemite s" の正式名称
|
|
12
|
+
# gemite start 3000 ポート指定
|
|
13
|
+
# gemite version バージョン表示
|
|
14
|
+
# gemite help ヘルプ表示
|
|
15
|
+
module CLI
|
|
16
|
+
DEFAULT_PORT = 8000
|
|
17
|
+
|
|
18
|
+
def self.run(argv)
|
|
19
|
+
cmd = argv[0]
|
|
20
|
+
|
|
21
|
+
case cmd
|
|
22
|
+
when nil, "s", "start"
|
|
23
|
+
start_server(argv[1])
|
|
24
|
+
when "version", "--version", "-v"
|
|
25
|
+
print_version
|
|
26
|
+
when "help", "--help", "-h"
|
|
27
|
+
print_help
|
|
28
|
+
else
|
|
29
|
+
warn "不明なコマンドです: #{cmd}"
|
|
30
|
+
warn ""
|
|
31
|
+
print_help
|
|
32
|
+
exit(1)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def self.start_server(port_arg)
|
|
37
|
+
port = parse_port(port_arg)
|
|
38
|
+
Gemite::HttpServer.new(root: Dir.pwd, port: port).start
|
|
39
|
+
end
|
|
40
|
+
private_class_method :start_server
|
|
41
|
+
|
|
42
|
+
def self.parse_port(arg)
|
|
43
|
+
return DEFAULT_PORT if arg.nil? || arg.strip.empty?
|
|
44
|
+
|
|
45
|
+
unless arg =~ /\A\d+\z/
|
|
46
|
+
warn "ポート番号が不正です: #{arg}"
|
|
47
|
+
exit(1)
|
|
48
|
+
end
|
|
49
|
+
arg.to_i
|
|
50
|
+
end
|
|
51
|
+
private_class_method :parse_port
|
|
52
|
+
|
|
53
|
+
def self.print_version
|
|
54
|
+
puts "Gemite #{Gemite::VERSION}"
|
|
55
|
+
end
|
|
56
|
+
private_class_method :print_version
|
|
57
|
+
|
|
58
|
+
def self.print_help
|
|
59
|
+
puts <<~HELP
|
|
60
|
+
Gemite #{Gemite::VERSION} - HTMLファーストな小規模Webアプリ言語
|
|
61
|
+
|
|
62
|
+
使い方:
|
|
63
|
+
gemite 開発サーバを起動する (http://localhost:8000)
|
|
64
|
+
gemite s 開発サーバを起動する
|
|
65
|
+
gemite s PORT ポートを指定して開発サーバを起動する
|
|
66
|
+
gemite start "gemite s" の正式名称
|
|
67
|
+
gemite start PORT ポートを指定して開発サーバを起動する
|
|
68
|
+
gemite version バージョンを表示する
|
|
69
|
+
gemite help このヘルプを表示する
|
|
70
|
+
|
|
71
|
+
現在のディレクトリを起点に .gmt ファイルをそのままURLへマッピングします。
|
|
72
|
+
index.gmt -> /
|
|
73
|
+
login.gmt -> /login
|
|
74
|
+
users/profile.gmt -> /users/profile
|
|
75
|
+
|
|
76
|
+
静的ファイルは public/ 以下に置いてください(例: public/css/style.css -> /css/style.css)。
|
|
77
|
+
HELP
|
|
78
|
+
end
|
|
79
|
+
private_class_method :print_help
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "sqlite3_ffi"
|
|
4
|
+
require_relative "errors"
|
|
5
|
+
|
|
6
|
+
module Gemite
|
|
7
|
+
# Gemiteコード上の `db = sqlite("app.db")` で得られるオブジェクトの実体。
|
|
8
|
+
# 仕様18〜22の select/insert/update/delete/exec をSQLに組み立てて実行する。
|
|
9
|
+
class Database
|
|
10
|
+
IDENTIFIER = /\A[A-Za-z_][A-Za-z0-9_]*\z/
|
|
11
|
+
|
|
12
|
+
def initialize(path)
|
|
13
|
+
@handle = SQLite3FFI::Handle.new(path)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# 生SQLを実行する(CREATE TABLE等、戻り値を使わない用途)。
|
|
17
|
+
def exec(sql)
|
|
18
|
+
@handle.exec(sql.to_s)
|
|
19
|
+
nil
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# 全件、または where: {col: val, ...} 条件でSELECTする。
|
|
23
|
+
def select(table, opts = {})
|
|
24
|
+
table_sql = quote_identifier(table)
|
|
25
|
+
where_hash = opts.is_a?(Hash) ? (opts[:where] || opts["where"]) : nil
|
|
26
|
+
clause, params = build_where(where_hash)
|
|
27
|
+
sql = "SELECT * FROM #{table_sql}#{clause}"
|
|
28
|
+
@handle.query(sql, params)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# {col: val, ...} をINSERTする。
|
|
32
|
+
def insert(table, values)
|
|
33
|
+
values = values || {}
|
|
34
|
+
table_sql = quote_identifier(table)
|
|
35
|
+
cols = values.keys
|
|
36
|
+
return nil if cols.empty?
|
|
37
|
+
|
|
38
|
+
col_sql = cols.map { |c| quote_identifier(c) }.join(", ")
|
|
39
|
+
placeholders = (["?"] * cols.length).join(", ")
|
|
40
|
+
sql = "INSERT INTO #{table_sql} (#{col_sql}) VALUES (#{placeholders})"
|
|
41
|
+
result = @handle.execute(sql, cols.map { |c| values[c] })
|
|
42
|
+
result[:rowid]
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# {col: val, ...} でUPDATEする。where: {col: val, ...} で対象を絞る。
|
|
46
|
+
def update(table, values, opts = {})
|
|
47
|
+
values = values || {}
|
|
48
|
+
table_sql = quote_identifier(table)
|
|
49
|
+
cols = values.keys
|
|
50
|
+
return 0 if cols.empty?
|
|
51
|
+
|
|
52
|
+
set_sql = cols.map { |c| "#{quote_identifier(c)} = ?" }.join(", ")
|
|
53
|
+
set_params = cols.map { |c| values[c] }
|
|
54
|
+
|
|
55
|
+
where_hash = opts.is_a?(Hash) ? (opts[:where] || opts["where"]) : nil
|
|
56
|
+
clause, where_params = build_where(where_hash)
|
|
57
|
+
|
|
58
|
+
sql = "UPDATE #{table_sql} SET #{set_sql}#{clause}"
|
|
59
|
+
result = @handle.execute(sql, set_params + where_params)
|
|
60
|
+
result[:changes]
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# where: {col: val, ...} に一致する行をDELETEする。
|
|
64
|
+
def delete(table, opts = {})
|
|
65
|
+
table_sql = quote_identifier(table)
|
|
66
|
+
where_hash = opts.is_a?(Hash) ? (opts[:where] || opts["where"]) : nil
|
|
67
|
+
clause, params = build_where(where_hash)
|
|
68
|
+
sql = "DELETE FROM #{table_sql}#{clause}"
|
|
69
|
+
result = @handle.execute(sql, params)
|
|
70
|
+
result[:changes]
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def close
|
|
74
|
+
@handle.close
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
# where: {a: 1, b: "x"} -> [" WHERE a = ? AND b = ?", [1, "x"]]
|
|
80
|
+
def build_where(where_hash)
|
|
81
|
+
return ["", []] if where_hash.nil? || where_hash.empty?
|
|
82
|
+
|
|
83
|
+
conds = []
|
|
84
|
+
params = []
|
|
85
|
+
where_hash.each do |col, val|
|
|
86
|
+
conds << "#{quote_identifier(col)} = ?"
|
|
87
|
+
params << val
|
|
88
|
+
end
|
|
89
|
+
[" WHERE #{conds.join(' AND ')}", params]
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def quote_identifier(name)
|
|
93
|
+
str = name.to_s
|
|
94
|
+
unless IDENTIFIER.match?(str)
|
|
95
|
+
raise Gemite::RuntimeError, "不正なテーブル名/カラム名です: #{str.inspect}"
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
"\"#{str}\""
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gemite
|
|
4
|
+
# 変数を保持するスコープ。
|
|
5
|
+
#
|
|
6
|
+
# スコープ規則(仕様25):
|
|
7
|
+
# - グローバル変数はファイル単位 -> ルートのEnvironment(boundary:false, parent:nil)を
|
|
8
|
+
# ファイル実行全体(<?gemite ?>ブロックをまたいでも)で共有する。
|
|
9
|
+
# - 関数内変数はローカル -> 関数呼び出し時に boundary: true の新しい環境を作る。
|
|
10
|
+
# assign(代入)はこの境界を越えて外側を書き換えない。ただし読み取り(get)は
|
|
11
|
+
# 外側(クロージャ)を辿れる。
|
|
12
|
+
# - @forのループ変数はループ内のみ有効 -> こちらはEnvironmentを増やさず、
|
|
13
|
+
# Interpreter側でループ変数名だけ退避/削除することで実現する(他の代入は
|
|
14
|
+
# 通常通り現在のスコープに作用する)。
|
|
15
|
+
class Environment
|
|
16
|
+
attr_reader :parent
|
|
17
|
+
|
|
18
|
+
def initialize(parent: nil, boundary: false)
|
|
19
|
+
@vars = {}
|
|
20
|
+
@parent = parent
|
|
21
|
+
@boundary = boundary
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def boundary?
|
|
25
|
+
@boundary
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def has_local?(name)
|
|
29
|
+
@vars.key?(name)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def get_local(name)
|
|
33
|
+
@vars[name]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def set_local(name, value)
|
|
37
|
+
@vars[name] = value
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def delete_local(name)
|
|
41
|
+
@vars.delete(name)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# 一般的な代入。既存の束縛が(関数境界を越えない範囲で)見つかればそこを更新し、
|
|
45
|
+
# 見つからなければ現在のスコープに新規作成する。
|
|
46
|
+
def assign(name, value)
|
|
47
|
+
env = self
|
|
48
|
+
loop do
|
|
49
|
+
if env.has_local?(name)
|
|
50
|
+
env.set_local(name, value)
|
|
51
|
+
return value
|
|
52
|
+
end
|
|
53
|
+
break if env.boundary? || env.parent.nil?
|
|
54
|
+
|
|
55
|
+
env = env.parent
|
|
56
|
+
end
|
|
57
|
+
set_local(name, value)
|
|
58
|
+
value
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# 変数の読み取り。関数境界を越えて外側(クロージャ)を辿る。
|
|
62
|
+
# 未定義の変数は nil (寛容な設計。GET/POST同様に「存在しなければnil」に統一)。
|
|
63
|
+
def get(name)
|
|
64
|
+
env = self
|
|
65
|
+
while env
|
|
66
|
+
return env.get_local(name) if env.has_local?(name)
|
|
67
|
+
|
|
68
|
+
env = env.parent
|
|
69
|
+
end
|
|
70
|
+
nil
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gemite
|
|
4
|
+
# 字句解析・構文解析エラー
|
|
5
|
+
class SyntaxError < StandardError
|
|
6
|
+
attr_reader :line
|
|
7
|
+
|
|
8
|
+
def initialize(message, line: nil)
|
|
9
|
+
@line = line
|
|
10
|
+
super(line ? "#{message} (#{line}行目)" : message)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Gemiteコード実行中のエラー。begin/catch の err にはこの例外オブジェクトが
|
|
15
|
+
# そのまま渡されるのではなく、to_s した文字列相当がユーザから見える値になる。
|
|
16
|
+
class RuntimeError < StandardError
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# def で定義した関数から return するための内部制御シグナル。
|
|
20
|
+
# 例外機構を使い、ネストしたif/forを一気に抜けて関数呼び出し境界まで戻る。
|
|
21
|
+
class ReturnSignal < StandardError
|
|
22
|
+
attr_reader :value
|
|
23
|
+
|
|
24
|
+
def initialize(value)
|
|
25
|
+
@value = value
|
|
26
|
+
super("return")
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# redirect() 呼び出しでリクエスト処理を打ち切り、HTTPリダイレクトを行うための内部シグナル。
|
|
31
|
+
class RedirectSignal < StandardError
|
|
32
|
+
attr_reader :location
|
|
33
|
+
|
|
34
|
+
def initialize(location)
|
|
35
|
+
@location = location
|
|
36
|
+
super("redirect")
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gemite
|
|
4
|
+
# def name(params) ... end で定義された関数の実行時表現。
|
|
5
|
+
# closure_env には定義時点で有効だったEnvironmentを保持し、
|
|
6
|
+
# 関数内から外側(グローバル等)の変数を読み取れるようにする(代入は関数内ローカルに留まる)。
|
|
7
|
+
class Function
|
|
8
|
+
attr_reader :name, :params, :body, :closure_env
|
|
9
|
+
|
|
10
|
+
def initialize(name, params, body, closure_env)
|
|
11
|
+
@name = name
|
|
12
|
+
@params = params
|
|
13
|
+
@body = body
|
|
14
|
+
@closure_env = closure_env
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def to_s
|
|
18
|
+
"<function #{name}>"
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "socket"
|
|
4
|
+
require_relative "request"
|
|
5
|
+
require_relative "router"
|
|
6
|
+
require_relative "template_parser"
|
|
7
|
+
require_relative "interpreter"
|
|
8
|
+
require_relative "errors"
|
|
9
|
+
|
|
10
|
+
module Gemite
|
|
11
|
+
# 依存ゼロ(Ruby標準の socket のみ)で実装した小規模開発用HTTPサーバ。
|
|
12
|
+
# .gmt ファイルはリクエストごとに読み直してパース・実行するため、
|
|
13
|
+
# 編集内容が即座に反映される(再起動不要)。
|
|
14
|
+
class HttpServer
|
|
15
|
+
def initialize(root:, port: 8000, host: "0.0.0.0")
|
|
16
|
+
@root = File.expand_path(root)
|
|
17
|
+
@port = port
|
|
18
|
+
@host = host
|
|
19
|
+
@router = Router.new(@root)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def start
|
|
23
|
+
$stdout.sync = true
|
|
24
|
+
server = TCPServer.new(@host, @port)
|
|
25
|
+
puts "Gemite dev server listening on http://localhost:#{@port}"
|
|
26
|
+
puts "root: #{@root}"
|
|
27
|
+
puts "(Ctrl-C で停止)"
|
|
28
|
+
|
|
29
|
+
trap("INT") do
|
|
30
|
+
puts "\nシャットダウンします..."
|
|
31
|
+
exit(0)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
loop do
|
|
35
|
+
client = server.accept
|
|
36
|
+
Thread.new(client) { |c| handle_client(c) }
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def handle_client(client)
|
|
43
|
+
request = read_request(client)
|
|
44
|
+
return unless request
|
|
45
|
+
|
|
46
|
+
handle_request(client, request)
|
|
47
|
+
rescue StandardError => e
|
|
48
|
+
begin
|
|
49
|
+
send_response(client, 500, "text/plain; charset=utf-8", "内部エラー: #{e.class}: #{e.message}")
|
|
50
|
+
rescue StandardError
|
|
51
|
+
nil
|
|
52
|
+
end
|
|
53
|
+
ensure
|
|
54
|
+
begin
|
|
55
|
+
client.close
|
|
56
|
+
rescue StandardError
|
|
57
|
+
nil
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def read_request(client)
|
|
62
|
+
request_line = client.gets
|
|
63
|
+
return nil if request_line.nil?
|
|
64
|
+
|
|
65
|
+
method, path, = request_line.strip.split(" ")
|
|
66
|
+
return nil unless method && path
|
|
67
|
+
|
|
68
|
+
headers = {}
|
|
69
|
+
loop do
|
|
70
|
+
line = client.gets
|
|
71
|
+
break if line.nil?
|
|
72
|
+
|
|
73
|
+
line = line.strip
|
|
74
|
+
break if line.empty?
|
|
75
|
+
|
|
76
|
+
key, val = line.split(":", 2)
|
|
77
|
+
next unless key && val
|
|
78
|
+
|
|
79
|
+
headers[key.strip.downcase] = val.strip
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
body = ""
|
|
83
|
+
len = headers["content-length"].to_i
|
|
84
|
+
body = client.read(len) if len.positive?
|
|
85
|
+
|
|
86
|
+
full_path, query_str = path.split("?", 2)
|
|
87
|
+
query_params = Request.parse_www_form(query_str || "")
|
|
88
|
+
|
|
89
|
+
Request.new(method: method, path: full_path, query_params: query_params, headers: headers, body: body)
|
|
90
|
+
rescue StandardError
|
|
91
|
+
nil
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def handle_request(client, request)
|
|
95
|
+
result = @router.resolve(request.path)
|
|
96
|
+
case result.type
|
|
97
|
+
when :gemite
|
|
98
|
+
handle_gemite_request(client, result.path, request)
|
|
99
|
+
when :static
|
|
100
|
+
handle_static_request(client, result.path)
|
|
101
|
+
else
|
|
102
|
+
send_response(client, 404, "text/html; charset=utf-8", not_found_html(request.path))
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def handle_gemite_request(client, file_path, request)
|
|
107
|
+
source = File.read(file_path, encoding: "UTF-8")
|
|
108
|
+
nodes = TemplateParser.parse(source)
|
|
109
|
+
interpreter = Interpreter.new(
|
|
110
|
+
get_params: request.query_params,
|
|
111
|
+
post_params: request.form_params,
|
|
112
|
+
base_dir: File.dirname(file_path)
|
|
113
|
+
)
|
|
114
|
+
html = interpreter.render(nodes)
|
|
115
|
+
send_response(client, 200, "text/html; charset=utf-8", html)
|
|
116
|
+
rescue Gemite::RedirectSignal => e
|
|
117
|
+
send_redirect(client, e.location)
|
|
118
|
+
rescue Gemite::SyntaxError, Gemite::RuntimeError => e
|
|
119
|
+
send_response(client, 500, "text/html; charset=utf-8", error_html(e, file_path))
|
|
120
|
+
rescue StandardError => e
|
|
121
|
+
send_response(client, 500, "text/html; charset=utf-8", error_html(e, file_path))
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def handle_static_request(client, file_path)
|
|
125
|
+
data = File.binread(file_path)
|
|
126
|
+
send_response(client, 200, @router.mime_type_for(file_path), data, binary: true)
|
|
127
|
+
rescue StandardError => e
|
|
128
|
+
send_response(client, 500, "text/plain; charset=utf-8", "静的ファイルの読み込みに失敗しました: #{e.message}")
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def send_response(client, status, content_type, body, binary: false)
|
|
132
|
+
bytes = binary ? body : body.to_s.b
|
|
133
|
+
status_text = STATUS_TEXT[status] || "Unknown"
|
|
134
|
+
client.write("HTTP/1.1 #{status} #{status_text}\r\n")
|
|
135
|
+
client.write("Content-Type: #{content_type}\r\n")
|
|
136
|
+
client.write("Content-Length: #{bytes.bytesize}\r\n")
|
|
137
|
+
client.write("Connection: close\r\n")
|
|
138
|
+
client.write("\r\n")
|
|
139
|
+
client.write(bytes)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def send_redirect(client, location)
|
|
143
|
+
client.write("HTTP/1.1 302 Found\r\n")
|
|
144
|
+
client.write("Location: #{location}\r\n")
|
|
145
|
+
client.write("Content-Length: 0\r\n")
|
|
146
|
+
client.write("Connection: close\r\n")
|
|
147
|
+
client.write("\r\n")
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
STATUS_TEXT = {
|
|
151
|
+
200 => "OK", 302 => "Found", 404 => "Not Found", 500 => "Internal Server Error"
|
|
152
|
+
}.freeze
|
|
153
|
+
|
|
154
|
+
def not_found_html(path)
|
|
155
|
+
<<~HTML
|
|
156
|
+
<!DOCTYPE html>
|
|
157
|
+
<html><head><meta charset="utf-8"><title>404 Not Found</title></head>
|
|
158
|
+
<body style="font-family: sans-serif; padding: 2rem;">
|
|
159
|
+
<h1>404 Not Found</h1>
|
|
160
|
+
<p><code>#{escape_html(path)}</code> に対応する .gmt ファイルが見つかりませんでした。</p>
|
|
161
|
+
</body></html>
|
|
162
|
+
HTML
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def error_html(error, file_path)
|
|
166
|
+
<<~HTML
|
|
167
|
+
<!DOCTYPE html>
|
|
168
|
+
<html><head><meta charset="utf-8"><title>500 Internal Server Error</title></head>
|
|
169
|
+
<body style="font-family: sans-serif; padding: 2rem;">
|
|
170
|
+
<h1>500 Internal Server Error</h1>
|
|
171
|
+
<p><strong>#{escape_html(error.class.to_s)}</strong>: #{escape_html(error.message)}</p>
|
|
172
|
+
<p style="color:#666">ファイル: <code>#{escape_html(file_path)}</code></p>
|
|
173
|
+
</body></html>
|
|
174
|
+
HTML
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def escape_html(str)
|
|
178
|
+
str.to_s.gsub("&", "&").gsub("<", "<").gsub(">", ">").gsub('"', """)
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|