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.
@@ -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
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gemite
4
+ # 字句解析の最小単位。type はSymbol、value はトークン種別ごとの実値、
5
+ # line はエラーメッセージ用の行番号。
6
+ Token = Struct.new(:type, :value, :line) do
7
+ def to_s
8
+ "#<Token #{type}:#{value.inspect} @#{line}>"
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gemite
4
+ VERSION = "0.1.0"
5
+ end
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: []