Class: Tina4::Frond
- Inherits:
-
Object
- Object
- Tina4::Frond
- Defined in:
- lib/tina4/frond.rb
Defined Under Namespace
Classes: LoopContext
Constant Summary collapse
- TEXT =
– Token types ———————————————————-
:text- VAR =
… }
:var- BLOCK =
… %
:block- COMMENT =
… #
:comment- TOKEN_RE =
Regex to split template source into tokens
/(\{%-?\s*.*?\s*-?%\})|(\{\{-?\s*.*?\s*-?\}\})|(\{#.*?#\})/m- HTML_ESCAPE_MAP =
HTML escape table
{ "&" => "&", "<" => "<", ">" => ">", '"' => """, "'" => "'" }.freeze
- HTML_ESCAPE_RE =
/[&<>"']/- EXTENDS_RE =
– Compiled regex constants (optimization: avoid re-compiling in methods) –
/\{%-?\s*extends\s+["'](.+?)["']\s*-?%\}/- BLOCK_RE =
/\{%-?\s*block\s+(\w+)\s*-?%\}(.*?)\{%-?\s*endblock\s*-?%\}/m- STRING_LIT_RE =
/\A["'](.*)["']\z/- INTEGER_RE =
/\A-?\d+\z/- FLOAT_RE =
/\A-?\d+\.\d+\z/- ARRAY_LIT_RE =
/\A\[(.+)\]\z/m- HASH_LIT_RE =
/\A\{(.+)\}\z/m- HASH_PAIR_RE =
/\A\s*(?:["']([^"']+)["']|(\w+))\s*:\s*(.+)\z/- RANGE_LIT_RE =
/\A(\d+)\.\.(\d+)\z/- ARITHMETIC_OPS =
[" + ", " - ", " * ", " // ", " / ", " % ", " ** "].freeze
- FUNC_CALL_RE =
/\A(\w+)\s*\((.*)\)\z/m- FILTER_WITH_ARGS_RE =
/\A(\w+)\s*\((.*)\)\z/m- FILTER_CMP_RE =
/\A(\w+)\s*(!=|==|>=|<=|>|<)\s*(.+)\z/- OR_SPLIT_RE =
/\s+or\s+/- AND_SPLIT_RE =
/\s+and\s+/- IS_NOT_RE =
/\A(.+?)\s+is\s+not\s+(\w+)(.*)\z/- IS_RE =
/\A(.+?)\s+is\s+(\w+)(.*)\z/- NOT_IN_RE =
/\A(.+?)\s+not\s+in\s+(.+)\z/- IN_RE =
/\A(.+?)\s+in\s+(.+)\z/- DIVISIBLE_BY_RE =
/\s*by\s*\(\s*(\d+)\s*\)/- RESOLVE_SPLIT_RE =
/\.|\[([^\]]+)\]/- RESOLVE_STRIP_RE =
/\A["']|["']\z/- DIGIT_RE =
/\A\d+\z/- FOR_RE =
/\Afor\s+(\w+)(?:\s*,\s*(\w+))?\s+in\s+(.+)\z/- SET_RE =
/\Aset\s+(\w+)\s*=\s*(.+)\z/m- INCLUDE_RE =
/\Ainclude\s+["'](.+?)["'](?:\s+with\s+(.+))?\z/- MACRO_RE =
/\Amacro\s+(\w+)\s*\(([^)]*)\)/- FROM_IMPORT_RE =
/\Afrom\s+["'](.+?)["']\s+import\s+(.+)/- CACHE_RE =
/\Acache\s+["'](.+?)["']\s*(\d+)?/- SPACELESS_RE =
/>\s+</- AUTOESCAPE_RE =
/\Aautoescape\s+(false|true)/- STRIPTAGS_RE =
/<[^>]+>/- THOUSANDS_RE =
/(\d)(?=(\d{3})+(?!\d))/- SLUG_CLEAN_RE =
/[^a-z0-9]+/- SLUG_TRIM_RE =
/\A-|-\z/- INLINE_FILTERS =
Set of common no-arg filter names that can be inlined for speed
%w[upper lower length trim capitalize title string int escape e].each_with_object({}) { |f, h| h[f] = true }.freeze
- @@class_filters =
– Class-level registries ———————————————— Persist globals, filters, and tests across hot-reloads and across module boundaries. When app.rb does “Tina4::Frond.add_filter(“money”) { … }“ at startup before any instance exists, the registration sits here. Every subsequent “Tina4::Frond.new“ drains these into its instance-local registries — so hot-reloads (which re-execute “frond = Frond.new“) and late-constructed engines automatically inherit prior registrations.
The same-name dual-callable (class + instance) methods below let callers write either “Tina4::Frond.add_filter(…)“ (class-level only) or “frond.add_filter(…)“ (updates both the class registry and the instance’s live filter map). Parity with tina4-python’s “_ClassOrInstanceMethod“ descriptor.
{}
- @@class_globals =
{}
- @@class_tests =
{}
Class Attribute Summary collapse
-
.form_token_session_id ⇒ Object
Returns the value of attribute form_token_session_id.
Instance Attribute Summary collapse
-
#template_dir ⇒ Object
readonly
———————————————————————– Public API ———————————————————————–.
Class Method Summary collapse
-
.add_filter(name, &blk) ⇒ Object
Register a custom filter on the class registry only.
-
.add_global(name, value) ⇒ Object
Register a global variable on the class registry only.
-
.add_test(name, &blk) ⇒ Object
Register a custom test on the class registry only.
-
.clear_registry ⇒ Object
Clear the class-level globals/filters/tests registries.
-
.escape_html(str) ⇒ Object
Utility: HTML escape.
-
.generate_form_jwt(descriptor = "") ⇒ String
Generate a raw JWT form token string.
- .generate_form_token(descriptor = "") ⇒ Object
-
.generate_form_token_value(descriptor = "") ⇒ Object
Return just the raw JWT form token string (no <input> wrapper).
-
.render_dump(value) ⇒ Object
Render a value as a pre-formatted inspect() wrapped in <pre> tags.
-
.set_form_token_session_id(session_id) ⇒ Object
Set the session ID used for CSRF form token binding.
Instance Method Summary collapse
-
#add_filter(name, &blk) ⇒ Object
Register a custom filter.
-
#add_global(name, value) ⇒ Object
Register a global variable available in all templates.
-
#add_test(name, &blk) ⇒ Object
Register a custom test.
-
#clear_cache ⇒ Object
Clear all compiled template caches.
-
#initialize(template_dir: "src/templates") ⇒ Frond
constructor
A new instance of Frond.
-
#render(template, data = {}) ⇒ Object
Render a template file with data.
-
#render_string(source, data = {}) ⇒ Object
Render a template string directly.
-
#sandbox(filters: nil, tags: nil, vars: nil) ⇒ Object
Enable sandbox mode.
-
#unsandbox ⇒ Object
Disable sandbox mode.
Constructor Details
#initialize(template_dir: "src/templates") ⇒ Frond
Returns a new instance of Frond.
186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 |
# File 'lib/tina4/frond.rb', line 186 def initialize(template_dir: "src/templates") @template_dir = template_dir @filters = default_filters @globals = {} @tests = default_tests @auto_escape = true # Sandboxing @sandbox = false @allowed_filters = nil @allowed_tags = nil @allowed_vars = nil # Fragment cache: key => [html, expires_at] @fragment_cache = {} # Token pre-compilation cache @compiled = {} # {template_name => [tokens, mtime]} @compiled_strings = {} # {md5_hash => tokens} # Parsed filter chain cache: expr_string => [variable, filters] @filter_chain_cache = {} # Resolved dotted-path split cache: expr_string => parts_array @resolve_cache = {} # Sandbox root-var split cache: var_name => root_var_string @dotted_split_cache = {} # Built-in global functions register_builtin_globals # Drain class-level registries into this instance. Filters and tests # registered via ``Tina4::Frond.add_filter`` BEFORE this instance was # constructed flow in here. Globals likewise. This is the key to the # static-facade: ``app.rb`` registers once at startup, and every # Frond instance created later (including those born from hot-reloads) # automatically inherits the registration. Parity with tina4-python. @filters.merge!(@@class_filters) @globals.merge!(@@class_globals) @tests.merge!(@@class_tests) end |
Class Attribute Details
.form_token_session_id ⇒ Object
Returns the value of attribute form_token_session_id.
2122 2123 2124 |
# File 'lib/tina4/frond.rb', line 2122 def form_token_session_id @form_token_session_id end |
Instance Attribute Details
#template_dir ⇒ Object (readonly)
Public API
184 185 186 |
# File 'lib/tina4/frond.rb', line 184 def template_dir @template_dir end |
Class Method Details
.add_filter(name, &blk) ⇒ Object
Register a custom filter on the class registry only.
Callable as “Tina4::Frond.add_filter(“money”) { |v| … }“ at app startup BEFORE any instance exists. The registration is remembered at class level so every later “Tina4::Frond.new“ inherits it. To also update a live instance’s filter map, use the instance method form.
295 296 297 |
# File 'lib/tina4/frond.rb', line 295 def self.add_filter(name, &blk) @@class_filters[name.to_s] = blk end |
.add_global(name, value) ⇒ Object
Register a global variable on the class registry only.
Same dual-callable semantics as “add_filter“ — see that method for the static-facade pattern.
311 312 313 |
# File 'lib/tina4/frond.rb', line 311 def self.add_global(name, value) @@class_globals[name.to_s] = value end |
.add_test(name, &blk) ⇒ Object
Register a custom test on the class registry only.
Same dual-callable semantics as “add_filter“ — see that method for the static-facade pattern.
303 304 305 |
# File 'lib/tina4/frond.rb', line 303 def self.add_test(name, &blk) @@class_tests[name.to_s] = blk end |
.clear_registry ⇒ Object
Clear the class-level globals/filters/tests registries.
Useful in test fixtures to prevent leaking state between tests. Does NOT affect built-in filters or globals — only user-registered ones.
45 46 47 48 49 |
# File 'lib/tina4/frond.rb', line 45 def self.clear_registry @@class_filters = {} @@class_globals = {} @@class_tests = {} end |
.escape_html(str) ⇒ Object
Utility: HTML escape
365 366 367 |
# File 'lib/tina4/frond.rb', line 365 def self.escape_html(str) str.to_s.gsub(HTML_ESCAPE_RE, HTML_ESCAPE_MAP) end |
.generate_form_jwt(descriptor = "") ⇒ String
Generate a raw JWT form token string.
2141 2142 2143 2144 2145 2146 2147 2148 2149 2150 2151 2152 2153 2154 2155 2156 2157 2158 2159 2160 2161 2162 2163 |
# File 'lib/tina4/frond.rb', line 2141 def self.generate_form_jwt(descriptor = "") require_relative "log" require_relative "auth" payload = { "type" => "form", "nonce" => SecureRandom.hex(8) } if descriptor && !descriptor.empty? if descriptor.include?("|") parts = descriptor.split("|", 2) payload["context"] = parts[0] payload["ref"] = parts[1] else payload["context"] = descriptor end end # Include session_id for CSRF session binding sid = form_token_session_id.to_s payload["session_id"] = sid unless sid.empty? ttl_minutes = (ENV["TINA4_TOKEN_LIMIT"] || "60").to_i expires_in = ttl_minutes * 60 Tina4::Auth.create_token(payload, expires_in: expires_in) end |
.generate_form_token(descriptor = "") ⇒ Object
2165 2166 2167 2168 |
# File 'lib/tina4/frond.rb', line 2165 def self.generate_form_token(descriptor = "") token = generate_form_jwt(descriptor) Tina4::SafeString.new(%(<input type="hidden" name="formToken" value="#{CGI.escapeHTML(token)}">)) end |
.generate_form_token_value(descriptor = "") ⇒ Object
Return just the raw JWT form token string (no <input> wrapper). Registered as both formTokenValue and form_token_value template globals.
2172 2173 2174 |
# File 'lib/tina4/frond.rb', line 2172 def self.generate_form_token_value(descriptor = "") Tina4::SafeString.new(generate_form_jwt(descriptor)) end |
.render_dump(value) ⇒ Object
Render a value as a pre-formatted inspect() wrapped in <pre> tags.
Gated on TINA4_DEBUG=true. In production (TINA4_DEBUG unset or false) this returns an empty SafeString to avoid leaking internal state, object shapes, or sensitive values into rendered HTML.
Shared by the value|dump } filter and the dump(value) } global function so both produce identical output and obey the same gating.
2097 2098 2099 2100 2101 2102 2103 2104 2105 2106 2107 |
# File 'lib/tina4/frond.rb', line 2097 def self.render_dump(value) return SafeString.new("") unless ENV.fetch("TINA4_DEBUG", "").downcase == "true" dumped = value.inspect escaped = dumped .gsub("&", "&") .gsub("<", "<") .gsub(">", ">") .gsub('"', """) SafeString.new("<pre>#{escaped}</pre>") end |
.set_form_token_session_id(session_id) ⇒ Object
Set the session ID used for CSRF form token binding. Parity with Python/PHP/Node: Frond.set_form_token_session_id(id)
2128 2129 2130 |
# File 'lib/tina4/frond.rb', line 2128 def set_form_token_session_id(session_id) self.form_token_session_id = session_id end |
Instance Method Details
#add_filter(name, &blk) ⇒ Object
Register a custom filter.
Updates BOTH the class registry (so future “Tina4::Frond.new“ picks the filter up) AND this instance’s live filter map (so the change is visible to subsequent renders on the current engine).
320 321 322 323 324 |
# File 'lib/tina4/frond.rb', line 320 def add_filter(name, &blk) self.class.add_filter(name, &blk) @filters[name.to_s] = blk self end |
#add_global(name, value) ⇒ Object
Register a global variable available in all templates.
Updates BOTH the class registry and this instance’s live globals map. See “add_filter“ for the dual-write semantics.
340 341 342 343 344 |
# File 'lib/tina4/frond.rb', line 340 def add_global(name, value) self.class.add_global(name, value) @globals[name.to_s] = value self end |
#add_test(name, &blk) ⇒ Object
Register a custom test.
Updates BOTH the class registry and this instance’s live tests map. See “add_filter“ for the dual-write semantics.
330 331 332 333 334 |
# File 'lib/tina4/frond.rb', line 330 def add_test(name, &blk) self.class.add_test(name, &blk) @tests[name.to_s] = blk self end |
#clear_cache ⇒ Object
Clear all compiled template caches.
281 282 283 284 285 286 287 |
# File 'lib/tina4/frond.rb', line 281 def clear_cache @compiled.clear @compiled_strings.clear @filter_chain_cache.clear @resolve_cache.clear @dotted_split_cache.clear end |
#render(template, data = {}) ⇒ Object
Render a template file with data. Uses token caching for performance.
Caching strategy:
* TINA4_DEBUG=true — never cache (always re-read + re-tokenize).
* TINA4_TEMPLATE_CACHE_TTL > 0 — cache entries expire after N seconds.
* TINA4_TEMPLATE_CACHE_TTL == 0 (default in production) — permanent cache.
235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 |
# File 'lib/tina4/frond.rb', line 235 def render(template, data = {}) context = @globals.merge(stringify_keys(data)) path = File.join(@template_dir, template) raise "Template not found: #{path}" unless File.exist?(path) debug_mode = ENV.fetch("TINA4_DEBUG", "").downcase == "true" ttl = (ENV["TINA4_TEMPLATE_CACHE_TTL"] || "0").to_i unless debug_mode cached = @compiled[template] if cached # cached layout: [tokens, mtime, cached_at] tokens, _mtime, cached_at = cached fresh = ttl <= 0 || (Time.now.to_i - cached_at.to_i) < ttl return execute_cached(tokens, context) if fresh end end # Dev mode: skip cache entirely — always re-read and re-tokenize # so edits to partials and extended base templates are detected # Cache miss — load, tokenize, cache source = File.read(path, encoding: "utf-8") mtime = File.mtime(path) tokens = tokenize(source) @compiled[template] = [tokens, mtime, Time.now.to_i] execute_with_tokens(source, tokens, context) end |
#render_string(source, data = {}) ⇒ Object
Render a template string directly. Uses token caching for performance.
265 266 267 268 269 270 271 272 273 274 275 276 277 278 |
# File 'lib/tina4/frond.rb', line 265 def render_string(source, data = {}) context = @globals.merge(stringify_keys(data)) key = Digest::MD5.hexdigest(source) cached_tokens = @compiled_strings[key] if cached_tokens return execute_cached(cached_tokens, context) end tokens = tokenize(source) @compiled_strings[key] = tokens execute_cached(tokens, context) end |
#sandbox(filters: nil, tags: nil, vars: nil) ⇒ Object
Enable sandbox mode.
347 348 349 350 351 352 353 |
# File 'lib/tina4/frond.rb', line 347 def sandbox(filters: nil, tags: nil, vars: nil) @sandbox = true @allowed_filters = filters ? filters.map(&:to_s) : nil @allowed_tags = ? .map(&:to_s) : nil @allowed_vars = vars ? vars.map(&:to_s) : nil self end |
#unsandbox ⇒ Object
Disable sandbox mode.
356 357 358 359 360 361 362 |
# File 'lib/tina4/frond.rb', line 356 def unsandbox @sandbox = false @allowed_filters = nil @allowed_tags = nil @allowed_vars = nil self end |