tina4ruby 0.4.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/CHANGELOG.md +80 -0
- data/LICENSE.txt +21 -0
- data/README.md +768 -0
- data/exe/tina4 +4 -0
- data/lib/tina4/api.rb +152 -0
- data/lib/tina4/auth.rb +139 -0
- data/lib/tina4/cli.rb +349 -0
- data/lib/tina4/crud.rb +124 -0
- data/lib/tina4/database.rb +135 -0
- data/lib/tina4/database_result.rb +89 -0
- data/lib/tina4/debug.rb +83 -0
- data/lib/tina4/dev.rb +15 -0
- data/lib/tina4/dev_reload.rb +68 -0
- data/lib/tina4/drivers/firebird_driver.rb +94 -0
- data/lib/tina4/drivers/mssql_driver.rb +112 -0
- data/lib/tina4/drivers/mysql_driver.rb +90 -0
- data/lib/tina4/drivers/postgres_driver.rb +99 -0
- data/lib/tina4/drivers/sqlite_driver.rb +85 -0
- data/lib/tina4/env.rb +55 -0
- data/lib/tina4/field_types.rb +84 -0
- data/lib/tina4/graphql.rb +837 -0
- data/lib/tina4/localization.rb +100 -0
- data/lib/tina4/middleware.rb +59 -0
- data/lib/tina4/migration.rb +124 -0
- data/lib/tina4/orm.rb +168 -0
- data/lib/tina4/public/css/tina4.css +2286 -0
- data/lib/tina4/public/css/tina4.min.css +2 -0
- data/lib/tina4/public/js/tina4.js +134 -0
- data/lib/tina4/public/js/tina4helper.js +387 -0
- data/lib/tina4/queue.rb +117 -0
- data/lib/tina4/queue_backends/kafka_backend.rb +80 -0
- data/lib/tina4/queue_backends/lite_backend.rb +79 -0
- data/lib/tina4/queue_backends/rabbitmq_backend.rb +73 -0
- data/lib/tina4/rack_app.rb +150 -0
- data/lib/tina4/request.rb +158 -0
- data/lib/tina4/response.rb +172 -0
- data/lib/tina4/router.rb +148 -0
- data/lib/tina4/scss/tina4css/_alerts.scss +34 -0
- data/lib/tina4/scss/tina4css/_badges.scss +22 -0
- data/lib/tina4/scss/tina4css/_buttons.scss +69 -0
- data/lib/tina4/scss/tina4css/_cards.scss +49 -0
- data/lib/tina4/scss/tina4css/_forms.scss +156 -0
- data/lib/tina4/scss/tina4css/_grid.scss +81 -0
- data/lib/tina4/scss/tina4css/_modals.scss +84 -0
- data/lib/tina4/scss/tina4css/_nav.scss +149 -0
- data/lib/tina4/scss/tina4css/_reset.scss +94 -0
- data/lib/tina4/scss/tina4css/_tables.scss +54 -0
- data/lib/tina4/scss/tina4css/_typography.scss +55 -0
- data/lib/tina4/scss/tina4css/_utilities.scss +197 -0
- data/lib/tina4/scss/tina4css/_variables.scss +117 -0
- data/lib/tina4/scss/tina4css/base.scss +1 -0
- data/lib/tina4/scss/tina4css/colors.scss +48 -0
- data/lib/tina4/scss/tina4css/tina4.scss +17 -0
- data/lib/tina4/scss_compiler.rb +131 -0
- data/lib/tina4/seeder.rb +529 -0
- data/lib/tina4/session.rb +145 -0
- data/lib/tina4/session_handlers/file_handler.rb +55 -0
- data/lib/tina4/session_handlers/mongo_handler.rb +49 -0
- data/lib/tina4/session_handlers/redis_handler.rb +43 -0
- data/lib/tina4/swagger.rb +123 -0
- data/lib/tina4/template.rb +478 -0
- data/lib/tina4/templates/base.twig +26 -0
- data/lib/tina4/templates/errors/403.twig +22 -0
- data/lib/tina4/templates/errors/404.twig +22 -0
- data/lib/tina4/templates/errors/500.twig +22 -0
- data/lib/tina4/testing.rb +213 -0
- data/lib/tina4/version.rb +5 -0
- data/lib/tina4/webserver.rb +101 -0
- data/lib/tina4/websocket.rb +167 -0
- data/lib/tina4/wsdl.rb +164 -0
- data/lib/tina4.rb +259 -0
- data/lib/tina4ruby.rb +4 -0
- metadata +324 -0
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tina4
|
|
4
|
+
module Template
|
|
5
|
+
TEMPLATE_DIRS = %w[templates src/templates src/views views].freeze
|
|
6
|
+
|
|
7
|
+
class << self
|
|
8
|
+
def globals
|
|
9
|
+
@globals ||= {}
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def add_global(key, value)
|
|
13
|
+
globals[key.to_s] = value
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def render(template_path, data = {})
|
|
17
|
+
full_path = resolve_path(template_path)
|
|
18
|
+
unless full_path && File.exist?(full_path)
|
|
19
|
+
raise "Template not found: #{template_path}"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
content = File.read(full_path)
|
|
23
|
+
ext = File.extname(full_path).downcase
|
|
24
|
+
context = globals.merge(data.transform_keys(&:to_s))
|
|
25
|
+
|
|
26
|
+
case ext
|
|
27
|
+
when ".twig", ".html", ".tina4"
|
|
28
|
+
TwigEngine.new(context, File.dirname(full_path)).render(content)
|
|
29
|
+
when ".erb"
|
|
30
|
+
ErbEngine.render(content, context)
|
|
31
|
+
else
|
|
32
|
+
TwigEngine.new(context, File.dirname(full_path)).render(content)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def render_error(code)
|
|
37
|
+
error_dirs = TEMPLATE_DIRS.map { |d| File.join(Dir.pwd, d, "errors") }
|
|
38
|
+
error_dirs << File.join(File.dirname(__FILE__), "templates", "errors")
|
|
39
|
+
|
|
40
|
+
error_dirs.each do |dir|
|
|
41
|
+
%w[.twig .html .erb].each do |ext|
|
|
42
|
+
path = File.join(dir, "#{code}#{ext}")
|
|
43
|
+
if File.exist?(path)
|
|
44
|
+
content = File.read(path)
|
|
45
|
+
return TwigEngine.new({ "code" => code }, dir).render(content)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
default_error_html(code)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def resolve_path(template_path)
|
|
55
|
+
return template_path if File.exist?(template_path)
|
|
56
|
+
TEMPLATE_DIRS.each do |dir|
|
|
57
|
+
full = File.join(Dir.pwd, dir, template_path)
|
|
58
|
+
return full if File.exist?(full)
|
|
59
|
+
end
|
|
60
|
+
gem_templates = File.join(File.dirname(__FILE__), "templates")
|
|
61
|
+
full = File.join(gem_templates, template_path)
|
|
62
|
+
return full if File.exist?(full)
|
|
63
|
+
nil
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def default_error_html(code)
|
|
67
|
+
messages = { 403 => "Forbidden", 404 => "Not Found", 500 => "Internal Server Error" }
|
|
68
|
+
msg = messages[code] || "Error"
|
|
69
|
+
"<!DOCTYPE html><html><head><title>#{code} #{msg}</title></head>" \
|
|
70
|
+
"<body style='font-family:sans-serif;text-align:center;padding:50px;'>" \
|
|
71
|
+
"<h1>#{code}</h1><p>#{msg}</p><hr>" \
|
|
72
|
+
"<p style='color:#999;'>Tina4 Ruby v#{Tina4::VERSION}</p></body></html>"
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
class TwigEngine
|
|
77
|
+
FILTERS = {
|
|
78
|
+
"upper" => ->(v) { v.to_s.upcase },
|
|
79
|
+
"lower" => ->(v) { v.to_s.downcase },
|
|
80
|
+
"capitalize" => ->(v) { v.to_s.capitalize },
|
|
81
|
+
"title" => ->(v) { v.to_s.split.map(&:capitalize).join(" ") },
|
|
82
|
+
"trim" => ->(v) { v.to_s.strip },
|
|
83
|
+
"length" => ->(v) { v.respond_to?(:length) ? v.length : v.to_s.length },
|
|
84
|
+
"reverse" => ->(v) { v.respond_to?(:reverse) ? v.reverse : v.to_s.reverse },
|
|
85
|
+
"first" => ->(v) { v.respond_to?(:first) ? v.first : v.to_s[0] },
|
|
86
|
+
"last" => ->(v) { v.respond_to?(:last) ? v.last : v.to_s[-1] },
|
|
87
|
+
"join" => ->(v, sep) { v.respond_to?(:join) ? v.join(sep || ", ") : v.to_s },
|
|
88
|
+
"default" => ->(v, d) { (v.nil? || v.to_s.empty?) ? d : v },
|
|
89
|
+
"escape" => ->(v) { TwigEngine.escape_html(v.to_s) },
|
|
90
|
+
"e" => ->(v) { TwigEngine.escape_html(v.to_s) },
|
|
91
|
+
"nl2br" => ->(v) { v.to_s.gsub("\n", "<br>") },
|
|
92
|
+
"number_format" => ->(v, d) { format("%.#{d || 0}f", v.to_f) },
|
|
93
|
+
"raw" => ->(v) { v },
|
|
94
|
+
"striptags" => ->(v) { v.to_s.gsub(/<[^>]+>/, "") },
|
|
95
|
+
"sort" => ->(v) { v.respond_to?(:sort) ? v.sort : v },
|
|
96
|
+
"keys" => ->(v) { v.respond_to?(:keys) ? v.keys : [] },
|
|
97
|
+
"values" => ->(v) { v.respond_to?(:values) ? v.values : [v] },
|
|
98
|
+
"abs" => ->(v) { v.to_f.abs },
|
|
99
|
+
"round" => ->(v, p) { v.to_f.round(p&.to_i || 0) },
|
|
100
|
+
"url_encode" => ->(v) { URI.encode_www_form_component(v.to_s) },
|
|
101
|
+
"json_encode" => ->(v) { JSON.generate(v) rescue v.to_s },
|
|
102
|
+
"slice" => ->(v, s, e) { v.to_s[(s.to_i)..(e ? e.to_i : -1)] },
|
|
103
|
+
"merge" => ->(v, o) { v.respond_to?(:merge) ? v.merge(o || {}) : v },
|
|
104
|
+
"batch" => ->(v, s) { v.respond_to?(:each_slice) ? v.each_slice(s.to_i).to_a : [v] },
|
|
105
|
+
"date" => ->(v, fmt) { TwigEngine.format_date(v, fmt) }
|
|
106
|
+
}.freeze
|
|
107
|
+
|
|
108
|
+
def initialize(context = {}, base_dir = nil)
|
|
109
|
+
@context = context
|
|
110
|
+
@base_dir = base_dir || Dir.pwd
|
|
111
|
+
@blocks = {}
|
|
112
|
+
@parent_template = nil
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def render(content)
|
|
116
|
+
content = process_extends(content)
|
|
117
|
+
content = process_blocks(content)
|
|
118
|
+
content = process_includes(content)
|
|
119
|
+
content = process_for_loops(content)
|
|
120
|
+
content = process_conditionals(content)
|
|
121
|
+
content = process_set(content)
|
|
122
|
+
content = process_expressions(content)
|
|
123
|
+
content = content.gsub(/\{%.*?%\}/m, "")
|
|
124
|
+
content = content.gsub(/\{#.*?#\}/m, "")
|
|
125
|
+
content
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def self.escape_html(str)
|
|
129
|
+
str.gsub("&", "&").gsub("<", "<").gsub(">", ">")
|
|
130
|
+
.gsub('"', """).gsub("'", "'")
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def self.format_date(value, fmt)
|
|
134
|
+
require "date"
|
|
135
|
+
d = value.is_a?(String) ? DateTime.parse(value) : value
|
|
136
|
+
d.respond_to?(:strftime) ? d.strftime(fmt || "%Y-%m-%d") : value.to_s
|
|
137
|
+
rescue
|
|
138
|
+
value.to_s
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
private
|
|
142
|
+
|
|
143
|
+
def process_extends(content)
|
|
144
|
+
if content =~ /\{%\s*extends\s+["'](.+?)["']\s*%\}/
|
|
145
|
+
parent_path = Regexp.last_match(1)
|
|
146
|
+
full_parent = resolve_template(parent_path)
|
|
147
|
+
if full_parent && File.exist?(full_parent)
|
|
148
|
+
@parent_template = File.read(full_parent)
|
|
149
|
+
content.scan(/\{%\s*block\s+(\w+)\s*%\}(.*?)\{%\s*endblock\s*%\}/m) do |name, body|
|
|
150
|
+
@blocks[name] = body
|
|
151
|
+
end
|
|
152
|
+
content = @parent_template
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
content
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def process_blocks(content)
|
|
159
|
+
content.gsub(/\{%\s*block\s+(\w+)\s*%\}(.*?)\{%\s*endblock\s*%\}/m) do
|
|
160
|
+
name = Regexp.last_match(1)
|
|
161
|
+
default_body = Regexp.last_match(2)
|
|
162
|
+
@blocks[name] || default_body
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def process_includes(content)
|
|
167
|
+
content.gsub(/\{%\s*include\s+["'](.+?)["'](?:\s+with\s+(.+?))?\s*%\}/) do
|
|
168
|
+
inc_path = Regexp.last_match(1)
|
|
169
|
+
full_path = resolve_template(inc_path)
|
|
170
|
+
if full_path && File.exist?(full_path)
|
|
171
|
+
inc_content = File.read(full_path)
|
|
172
|
+
TwigEngine.new(@context.dup, File.dirname(full_path)).render(inc_content)
|
|
173
|
+
else
|
|
174
|
+
"<!-- include not found: #{inc_path} -->"
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def process_for_loops(content)
|
|
180
|
+
max_depth = 10
|
|
181
|
+
depth = 0
|
|
182
|
+
while content =~ /\{%\s*for\s+/ && depth < max_depth
|
|
183
|
+
content = content.gsub(/\{%\s*for\s+(\w+)(?:\s*,\s*(\w+))?\s+in\s+(.+?)\s*%\}(.*?)\{%\s*endfor\s*%\}/m) do
|
|
184
|
+
key_or_val = Regexp.last_match(1)
|
|
185
|
+
val_name = Regexp.last_match(2)
|
|
186
|
+
collection_expr = Regexp.last_match(3)
|
|
187
|
+
body = Regexp.last_match(4)
|
|
188
|
+
collection = evaluate_expression(collection_expr)
|
|
189
|
+
output = ""
|
|
190
|
+
items = case collection
|
|
191
|
+
when Array then collection
|
|
192
|
+
when Hash then collection.to_a
|
|
193
|
+
when Range then collection.to_a
|
|
194
|
+
when Integer then (0...collection).to_a
|
|
195
|
+
else []
|
|
196
|
+
end
|
|
197
|
+
items.each_with_index do |item, index|
|
|
198
|
+
loop_context = @context.dup
|
|
199
|
+
loop_context["loop"] = {
|
|
200
|
+
"index" => index + 1, "index0" => index,
|
|
201
|
+
"first" => index == 0, "last" => index == items.length - 1,
|
|
202
|
+
"length" => items.length,
|
|
203
|
+
"revindex" => items.length - index,
|
|
204
|
+
"revindex0" => items.length - index - 1
|
|
205
|
+
}
|
|
206
|
+
if val_name
|
|
207
|
+
loop_context[key_or_val] = item.is_a?(Array) ? item[0] : index
|
|
208
|
+
loop_context[val_name] = item.is_a?(Array) ? item[1] : item
|
|
209
|
+
else
|
|
210
|
+
loop_context[key_or_val] = item
|
|
211
|
+
end
|
|
212
|
+
sub_engine = TwigEngine.new(loop_context, @base_dir)
|
|
213
|
+
output += sub_engine.render(body)
|
|
214
|
+
end
|
|
215
|
+
output
|
|
216
|
+
end
|
|
217
|
+
depth += 1
|
|
218
|
+
end
|
|
219
|
+
content
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def process_conditionals(content)
|
|
223
|
+
max_depth = 10
|
|
224
|
+
depth = 0
|
|
225
|
+
while content =~ /\{%\s*if\s+/ && depth < max_depth
|
|
226
|
+
content = content.gsub(
|
|
227
|
+
/\{%\s*if\s+(.+?)\s*%\}(.*?)(?:\{%\s*else\s*%\}(.*?))?\{%\s*endif\s*%\}/m
|
|
228
|
+
) do
|
|
229
|
+
condition = Regexp.last_match(1)
|
|
230
|
+
true_body = Regexp.last_match(2)
|
|
231
|
+
false_body = Regexp.last_match(3) || ""
|
|
232
|
+
|
|
233
|
+
if true_body =~ /\{%\s*elseif\s+/
|
|
234
|
+
segments = true_body.split(/\{%\s*elseif\s+/)
|
|
235
|
+
if evaluate_condition(condition)
|
|
236
|
+
segments[0]
|
|
237
|
+
else
|
|
238
|
+
resolved = false
|
|
239
|
+
result = ""
|
|
240
|
+
segments[1..].each do |seg|
|
|
241
|
+
next if resolved
|
|
242
|
+
if seg =~ /\A(.+?)\s*%\}(.*)\z/m
|
|
243
|
+
if evaluate_condition(Regexp.last_match(1))
|
|
244
|
+
result = Regexp.last_match(2)
|
|
245
|
+
resolved = true
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
resolved ? result : false_body
|
|
250
|
+
end
|
|
251
|
+
elsif evaluate_condition(condition)
|
|
252
|
+
true_body
|
|
253
|
+
else
|
|
254
|
+
false_body
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
depth += 1
|
|
258
|
+
end
|
|
259
|
+
content
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def process_set(content)
|
|
263
|
+
content.gsub(/\{%\s*set\s+(\w+)\s*=\s*(.+?)\s*%\}/) do
|
|
264
|
+
var_name = Regexp.last_match(1)
|
|
265
|
+
expr = Regexp.last_match(2)
|
|
266
|
+
@context[var_name] = evaluate_expression(expr)
|
|
267
|
+
""
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def process_expressions(content)
|
|
272
|
+
content.gsub(/\{\{\s*(.+?)\s*\}\}/) do
|
|
273
|
+
expr = Regexp.last_match(1)
|
|
274
|
+
evaluate_piped_expression(expr).to_s
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def evaluate_piped_expression(expr)
|
|
279
|
+
parts = expr.split("|").map(&:strip)
|
|
280
|
+
value = evaluate_expression(parts[0])
|
|
281
|
+
parts[1..].each do |filter_expr|
|
|
282
|
+
if filter_expr =~ /\A(\w+)(?:\((.+?)\))?\z/
|
|
283
|
+
filter_name = Regexp.last_match(1)
|
|
284
|
+
args_str = Regexp.last_match(2)
|
|
285
|
+
args = args_str ? parse_filter_args(args_str) : []
|
|
286
|
+
filter = FILTERS[filter_name]
|
|
287
|
+
value = args.empty? ? filter.call(value) : filter.call(value, *args) if filter
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
value
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def parse_filter_args(args_str)
|
|
294
|
+
args = []
|
|
295
|
+
current = ""
|
|
296
|
+
in_quote = nil
|
|
297
|
+
args_str.each_char do |ch|
|
|
298
|
+
if in_quote
|
|
299
|
+
if ch == in_quote
|
|
300
|
+
current += ch
|
|
301
|
+
in_quote = nil
|
|
302
|
+
else
|
|
303
|
+
current += ch
|
|
304
|
+
end
|
|
305
|
+
elsif ch == '"' || ch == "'"
|
|
306
|
+
in_quote = ch
|
|
307
|
+
current += ch
|
|
308
|
+
elsif ch == ","
|
|
309
|
+
args << current.strip
|
|
310
|
+
current = ""
|
|
311
|
+
else
|
|
312
|
+
current += ch
|
|
313
|
+
end
|
|
314
|
+
end
|
|
315
|
+
args << current.strip unless current.strip.empty?
|
|
316
|
+
|
|
317
|
+
args.map do |arg|
|
|
318
|
+
if arg =~ /\A["'](.*)["']\z/
|
|
319
|
+
Regexp.last_match(1)
|
|
320
|
+
elsif arg =~ /\A\d+\z/
|
|
321
|
+
arg.to_i
|
|
322
|
+
elsif arg =~ /\A\d+\.\d+\z/
|
|
323
|
+
arg.to_f
|
|
324
|
+
else
|
|
325
|
+
evaluate_expression(arg)
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def evaluate_expression(expr)
|
|
331
|
+
expr = expr.strip
|
|
332
|
+
return Regexp.last_match(1) if expr =~ /\A"([^"]*)"\z/ || expr =~ /\A'([^']*)'\z/
|
|
333
|
+
return expr.to_i if expr =~ /\A-?\d+\z/
|
|
334
|
+
return expr.to_f if expr =~ /\A-?\d+\.\d+\z/
|
|
335
|
+
return true if expr == "true"
|
|
336
|
+
return false if expr == "false"
|
|
337
|
+
return nil if expr == "null" || expr == "none" || expr == "nil"
|
|
338
|
+
if expr =~ /\A\[(.+)\]\z/
|
|
339
|
+
return Regexp.last_match(1).split(",").map { |i| evaluate_expression(i.strip) }
|
|
340
|
+
end
|
|
341
|
+
if expr =~ /\A(\d+)\.\.(\d+)\z/
|
|
342
|
+
return (Regexp.last_match(1).to_i..Regexp.last_match(2).to_i)
|
|
343
|
+
end
|
|
344
|
+
if expr.include?("~")
|
|
345
|
+
parts = expr.split("~").map { |p| evaluate_expression(p.strip) }
|
|
346
|
+
return parts.map(&:to_s).join
|
|
347
|
+
end
|
|
348
|
+
if expr =~ /\A(.+?)\s*(\+|-|\*|\/|%)\s*(.+)\z/
|
|
349
|
+
left = evaluate_expression(Regexp.last_match(1))
|
|
350
|
+
op = Regexp.last_match(2)
|
|
351
|
+
right = evaluate_expression(Regexp.last_match(3))
|
|
352
|
+
return apply_math(left, op, right)
|
|
353
|
+
end
|
|
354
|
+
resolve_variable(expr)
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
def resolve_variable(expr)
|
|
358
|
+
parts = expr.split(".")
|
|
359
|
+
value = @context
|
|
360
|
+
parts.each do |part|
|
|
361
|
+
if part =~ /\A(\w+)\[(.+?)\]\z/
|
|
362
|
+
base = Regexp.last_match(1)
|
|
363
|
+
index = Regexp.last_match(2)
|
|
364
|
+
value = access_value(value, base)
|
|
365
|
+
if index =~ /\A\d+\z/
|
|
366
|
+
value = value[index.to_i] if value.respond_to?(:[])
|
|
367
|
+
else
|
|
368
|
+
index = index.gsub(/["']/, "")
|
|
369
|
+
value = access_value(value, index)
|
|
370
|
+
end
|
|
371
|
+
else
|
|
372
|
+
value = access_value(value, part)
|
|
373
|
+
end
|
|
374
|
+
return nil if value.nil?
|
|
375
|
+
end
|
|
376
|
+
value
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
def access_value(obj, key)
|
|
380
|
+
return nil if obj.nil?
|
|
381
|
+
if obj.is_a?(Hash)
|
|
382
|
+
obj[key] || obj[key.to_sym] || obj[key.to_s]
|
|
383
|
+
elsif obj.respond_to?(key.to_sym)
|
|
384
|
+
obj.send(key.to_sym)
|
|
385
|
+
elsif obj.respond_to?(:[])
|
|
386
|
+
obj[key] rescue nil
|
|
387
|
+
end
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
def evaluate_condition(expr)
|
|
391
|
+
expr = expr.strip
|
|
392
|
+
return !evaluate_condition(Regexp.last_match(1)) if expr =~ /\Anot\s+(.+)\z/
|
|
393
|
+
if expr.include?(" and ")
|
|
394
|
+
return expr.split(/\s+and\s+/).all? { |p| evaluate_condition(p) }
|
|
395
|
+
end
|
|
396
|
+
if expr.include?(" or ")
|
|
397
|
+
return expr.split(/\s+or\s+/).any? { |p| evaluate_condition(p) }
|
|
398
|
+
end
|
|
399
|
+
if expr =~ /\A(.+?)\s*(==|!=|>=|<=|>|<)\s*(.+)\z/
|
|
400
|
+
left = evaluate_expression(Regexp.last_match(1).strip)
|
|
401
|
+
op = Regexp.last_match(2).strip
|
|
402
|
+
right = evaluate_expression(Regexp.last_match(3).strip)
|
|
403
|
+
return compare(left, op, right)
|
|
404
|
+
end
|
|
405
|
+
if expr =~ /\A(.+?)\s+in\s+(.+)\z/
|
|
406
|
+
needle = evaluate_expression(Regexp.last_match(1).strip)
|
|
407
|
+
haystack = evaluate_expression(Regexp.last_match(2).strip)
|
|
408
|
+
return haystack.respond_to?(:include?) ? haystack.include?(needle) : false
|
|
409
|
+
end
|
|
410
|
+
if expr =~ /\A(.+?)\s+is\s+defined\z/
|
|
411
|
+
return !evaluate_expression(Regexp.last_match(1).strip).nil?
|
|
412
|
+
end
|
|
413
|
+
if expr =~ /\A(.+?)\s+is\s+empty\z/
|
|
414
|
+
val = evaluate_expression(Regexp.last_match(1).strip)
|
|
415
|
+
return val.nil? || (val.respond_to?(:empty?) && val.empty?)
|
|
416
|
+
end
|
|
417
|
+
truthy?(evaluate_expression(expr))
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
def compare(left, op, right)
|
|
421
|
+
case op
|
|
422
|
+
when "==" then left == right
|
|
423
|
+
when "!=" then left != right
|
|
424
|
+
when ">" then left.to_f > right.to_f
|
|
425
|
+
when "<" then left.to_f < right.to_f
|
|
426
|
+
when ">=" then left.to_f >= right.to_f
|
|
427
|
+
when "<=" then left.to_f <= right.to_f
|
|
428
|
+
else false
|
|
429
|
+
end
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
def apply_math(left, op, right)
|
|
433
|
+
l = left.to_f
|
|
434
|
+
r = right.to_f
|
|
435
|
+
case op
|
|
436
|
+
when "+" then l + r
|
|
437
|
+
when "-" then l - r
|
|
438
|
+
when "*" then l * r
|
|
439
|
+
when "/" then r != 0 ? l / r : 0
|
|
440
|
+
when "%" then l % r
|
|
441
|
+
else 0
|
|
442
|
+
end
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
def truthy?(val)
|
|
446
|
+
return false if val.nil? || val == false || val == 0 || val == ""
|
|
447
|
+
return false if val.respond_to?(:empty?) && val.empty?
|
|
448
|
+
true
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
def resolve_template(path)
|
|
452
|
+
full = File.join(@base_dir, path)
|
|
453
|
+
return full if File.exist?(full)
|
|
454
|
+
Tina4::Template::TEMPLATE_DIRS.each do |dir|
|
|
455
|
+
candidate = File.join(Dir.pwd, dir, path)
|
|
456
|
+
return candidate if File.exist?(candidate)
|
|
457
|
+
end
|
|
458
|
+
nil
|
|
459
|
+
end
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
class ErbEngine
|
|
463
|
+
def self.render(content, context)
|
|
464
|
+
require "erb"
|
|
465
|
+
binding_obj = create_binding(context)
|
|
466
|
+
ERB.new(content, trim_mode: "-").result(binding_obj)
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
def self.create_binding(context)
|
|
470
|
+
b = binding
|
|
471
|
+
context.each do |key, value|
|
|
472
|
+
b.local_variable_set(key.to_sym, value)
|
|
473
|
+
end
|
|
474
|
+
b
|
|
475
|
+
end
|
|
476
|
+
end
|
|
477
|
+
end
|
|
478
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>{% block title %}Tina4 Ruby{% endblock %}</title>
|
|
7
|
+
<link rel="stylesheet" href="/css/tina4.min.css">
|
|
8
|
+
{% block head %}{% endblock %}
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
|
12
|
+
<div class="container">
|
|
13
|
+
<a class="navbar-brand" href="/">Tina4</a>
|
|
14
|
+
</div>
|
|
15
|
+
</nav>
|
|
16
|
+
<main class="container mt-4">
|
|
17
|
+
{% block content %}{% endblock %}
|
|
18
|
+
</main>
|
|
19
|
+
<footer class="container mt-4 py-3 text-center text-muted border-top">
|
|
20
|
+
<p>Powered by Tina4 Ruby v{{ tina4_version }}</p>
|
|
21
|
+
</footer>
|
|
22
|
+
<script src="/js/tina4.js"></script>
|
|
23
|
+
<script src="/js/tina4helper.js"></script>
|
|
24
|
+
{% block scripts %}{% endblock %}
|
|
25
|
+
</body>
|
|
26
|
+
</html>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>403 Forbidden</title>
|
|
7
|
+
<style>
|
|
8
|
+
body { font-family: -apple-system, sans-serif; text-align: center; padding: 80px 20px; background: #f8f9fa; }
|
|
9
|
+
h1 { font-size: 6rem; color: #dc3545; margin: 0; }
|
|
10
|
+
p { font-size: 1.2rem; color: #6c757d; }
|
|
11
|
+
a { color: #0d6efd; text-decoration: none; }
|
|
12
|
+
.container { max-width: 600px; margin: 0 auto; }
|
|
13
|
+
</style>
|
|
14
|
+
</head>
|
|
15
|
+
<body>
|
|
16
|
+
<div class="container">
|
|
17
|
+
<h1>403</h1>
|
|
18
|
+
<p>Forbidden. You do not have permission to access this resource.</p>
|
|
19
|
+
<p><a href="/">Go Home</a></p>
|
|
20
|
+
</div>
|
|
21
|
+
</body>
|
|
22
|
+
</html>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>404 Not Found</title>
|
|
7
|
+
<style>
|
|
8
|
+
body { font-family: -apple-system, sans-serif; text-align: center; padding: 80px 20px; background: #f8f9fa; }
|
|
9
|
+
h1 { font-size: 6rem; color: #ffc107; margin: 0; }
|
|
10
|
+
p { font-size: 1.2rem; color: #6c757d; }
|
|
11
|
+
a { color: #0d6efd; text-decoration: none; }
|
|
12
|
+
.container { max-width: 600px; margin: 0 auto; }
|
|
13
|
+
</style>
|
|
14
|
+
</head>
|
|
15
|
+
<body>
|
|
16
|
+
<div class="container">
|
|
17
|
+
<h1>404</h1>
|
|
18
|
+
<p>Page not found. The requested resource could not be located.</p>
|
|
19
|
+
<p><a href="/">Go Home</a></p>
|
|
20
|
+
</div>
|
|
21
|
+
</body>
|
|
22
|
+
</html>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>500 Internal Server Error</title>
|
|
7
|
+
<style>
|
|
8
|
+
body { font-family: -apple-system, sans-serif; text-align: center; padding: 80px 20px; background: #f8f9fa; }
|
|
9
|
+
h1 { font-size: 6rem; color: #dc3545; margin: 0; }
|
|
10
|
+
p { font-size: 1.2rem; color: #6c757d; }
|
|
11
|
+
a { color: #0d6efd; text-decoration: none; }
|
|
12
|
+
.container { max-width: 600px; margin: 0 auto; }
|
|
13
|
+
</style>
|
|
14
|
+
</head>
|
|
15
|
+
<body>
|
|
16
|
+
<div class="container">
|
|
17
|
+
<h1>500</h1>
|
|
18
|
+
<p>Internal Server Error. Something went wrong on our end.</p>
|
|
19
|
+
<p><a href="/">Go Home</a></p>
|
|
20
|
+
</div>
|
|
21
|
+
</body>
|
|
22
|
+
</html>
|