module Tina4::Template
def add_global(key, value)
def add_global(key, value) globals[key.to_s] = value end
def default_error_html(code)
def default_error_html(code) messages = { 403 => "Forbidden", 404 => "Not Found", 500 => "Internal Server Error" } msg = messages[code] || "Error" colors = { 403 => "#f59e0b", 404 => "#3b82f6", 500 => "#ef4444" } color = colors[code] || "#ef4444" <<~HTML <!DOCTYPE html> <html lang="en"> #{error_overlay_head("#{code} — #{msg}")} <body> #{error_overlay_css(color)} <div class="error-card"> <div class="logo">T4</div> <div class="error-code">#{code}</div> <div class="error-title">#{msg}</div> <div class="error-msg">Something went wrong while processing your request.</div> <a href="/" class="error-home">Go Home</a> </div> </body> </html> HTML end
def error_overlay_css(color)
def error_overlay_css(color) <<~CSS <style> * { box-sizing: border-box; margin: 0; padding: 0; } body { font-family: system-ui, -apple-system, sans-serif; background: #0f172a; color: #e2e8f0; min-height: 100vh; display: flex; align-items: center; justify-content: center; } .error-card { background: #1e293b; border: 1px solid #334155; border-radius: 1rem; padding: 3rem; text-align: center; max-width: 520px; width: 90%; } .error-code { font-size: 8rem; font-weight: 900; color: #{color}; opacity: 0.6; line-height: 1; margin-bottom: 0.5rem; } .error-title { font-size: 1.5rem; font-weight: 700; margin-bottom: 0.75rem; } .error-msg { color: #94a3b8; font-size: 1rem; margin-bottom: 1.5rem; line-height: 1.5; } .error-home { display: inline-block; padding: 0.6rem 2rem; background: #3b82f6; color: #fff; text-decoration: none; border-radius: 0.5rem; font-size: 0.9rem; font-weight: 600; } .error-home:hover { opacity: 0.9; } .logo { font-size: 1.5rem; margin-bottom: 1rem; opacity: 0.5; } </style> CSS end
def error_overlay_env
def error_overlay_env "<div class=\"env-info\">Ruby #{RUBY_VERSION} | Tina4</div>" end
def error_overlay_head(title)
def error_overlay_head(title) <<~HEAD <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>#{title}</title> </head> HEAD end
def error_overlay_request(env)
def error_overlay_request(env) return "" unless env.is_a?(Hash) method = env["REQUEST_METHOD"] || "?" path = env["PATH_INFO"] || "?" "<div class=\"request-info\"><strong>#{method}</strong> #{path}</div>" end
def error_overlay_source(file, line)
def error_overlay_source(file, line) return "" unless file && line && File.exist?(file) lines = File.readlines(file) start = [line.to_i - 4, 0].max finish = [line.to_i + 3, lines.length - 1].min snippet = lines[start..finish].each_with_index.map do |l, i| num = start + i + 1 "<div class=\"source-line#{num == line.to_i ? ' highlight' : ''}\"><span class=\"line-num\">#{num}</span>#{l.chomp}</div>" end.join("\n") "<pre class=\"source-context\">#{snippet}</pre>" end
def error_overlay_stacktrace(exception)
def error_overlay_stacktrace(exception) return "" unless exception.respond_to?(:backtrace) && exception.backtrace lines = exception.backtrace.map { |l| "<li>#{l}</li>" }.join("\n") "<ul class=\"stacktrace\">#{lines}</ul>" end
def globals
def globals @globals ||= {} end
def render(template_path, data = {})
def render(template_path, data = {}) full_path = resolve_path(template_path) unless full_path && File.exist?(full_path) raise "Template not found: #{template_path}" end content = File.read(full_path) ext = File.extname(full_path).downcase context = globals.merge(data.transform_keys(&:to_s)) case ext when ".twig", ".html", ".tina4" TwigEngine.new(context, File.dirname(full_path)).render(content) when ".erb" ErbEngine.render(content, context) else TwigEngine.new(context, File.dirname(full_path)).render(content) end end
def render_error(code, data = {})
def render_error(code, data = {}) error_dirs = TEMPLATE_DIRS.map { |d| File.join(Dir.pwd, d, "errors") } error_dirs << File.join(File.dirname(__FILE__), "templates", "errors") context = { "code" => code }.merge(data.transform_keys(&:to_s)) error_dirs.each do |dir| %w[.twig .html .erb].each do |ext| path = File.join(dir, "#{code}#{ext}") if File.exist?(path) content = File.read(path) return TwigEngine.new(context, dir).render(content) end end end default_error_html(code) end
def resolve_path(template_path)
def resolve_path(template_path) return template_path if File.exist?(template_path) TEMPLATE_DIRS.each do |dir| full = File.join(Dir.pwd, dir, template_path) return full if File.exist?(full) end gem_templates = File.join(File.dirname(__FILE__), "templates") full = File.join(gem_templates, template_path) return full if File.exist?(full) nil end