Class: Tina4::Frond

Inherits:
Object
  • Object
show all
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

{ "&" => "&amp;", "<" => "&lt;", ">" => "&gt;",
'"' => "&quot;", "'" => "&#39;" }.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 Attribute Summary collapse

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(template_dir: "src/templates") ⇒ Frond

Returns a new instance of Frond.



159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
# File 'lib/tina4/frond.rb', line 159

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
end

Class Attribute Details

.form_token_session_idObject

Returns the value of attribute form_token_session_id.



2043
2044
2045
# File 'lib/tina4/frond.rb', line 2043

def form_token_session_id
  @form_token_session_id
end

Instance Attribute Details

#template_dirObject (readonly)


Public API




157
158
159
# File 'lib/tina4/frond.rb', line 157

def template_dir
  @template_dir
end

Class Method Details

.escape_html(str) ⇒ Object

Utility: HTML escape



286
287
288
# File 'lib/tina4/frond.rb', line 286

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.

Parameters:

  • descriptor (String) (defaults to: "")

    Optional string to enrich the token payload.

    • Empty: payload is => “form”

    • “admin_panel”: payload is => “form”, “context” => “admin_panel”

    • “checkout|order_123”: payload is => “form”, “context” => “checkout”, “ref” => “order_123”

Returns:

  • (String)

    The raw JWT token string.



2062
2063
2064
2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
# File 'lib/tina4/frond.rb', line 2062

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



2086
2087
2088
2089
# File 'lib/tina4/frond.rb', line 2086

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.



2093
2094
2095
# File 'lib/tina4/frond.rb', line 2093

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.



2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
# File 'lib/tina4/frond.rb', line 2018

def self.render_dump(value)
  return SafeString.new("") unless ENV.fetch("TINA4_DEBUG", "").downcase == "true"

  dumped = value.inspect
  escaped = dumped
    .gsub("&", "&amp;")
    .gsub("<", "&lt;")
    .gsub(">", "&gt;")
    .gsub('"', "&quot;")
  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)

Parameters:

  • session_id (String)

    The session ID to bind form tokens to



2049
2050
2051
# File 'lib/tina4/frond.rb', line 2049

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.



253
254
255
# File 'lib/tina4/frond.rb', line 253

def add_filter(name, &blk)
  @filters[name.to_s] = blk
end

#add_global(name, value) ⇒ Object

Register a global variable available in all templates.



263
264
265
# File 'lib/tina4/frond.rb', line 263

def add_global(name, value)
  @globals[name.to_s] = value
end

#add_test(name, &blk) ⇒ Object

Register a custom test.



258
259
260
# File 'lib/tina4/frond.rb', line 258

def add_test(name, &blk)
  @tests[name.to_s] = blk
end

#clear_cacheObject

Clear all compiled template caches.



244
245
246
247
248
249
250
# File 'lib/tina4/frond.rb', line 244

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.


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
# File 'lib/tina4/frond.rb', line 198

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.



228
229
230
231
232
233
234
235
236
237
238
239
240
241
# File 'lib/tina4/frond.rb', line 228

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.



268
269
270
271
272
273
274
# File 'lib/tina4/frond.rb', line 268

def sandbox(filters: nil, tags: nil, vars: nil)
  @sandbox         = true
  @allowed_filters = filters ? filters.map(&:to_s) : nil
  @allowed_tags    = tags    ? tags.map(&:to_s)    : nil
  @allowed_vars    = vars    ? vars.map(&:to_s)    : nil
  self
end

#unsandboxObject

Disable sandbox mode.



277
278
279
280
281
282
283
# File 'lib/tina4/frond.rb', line 277

def unsandbox
  @sandbox         = false
  @allowed_filters = nil
  @allowed_tags    = nil
  @allowed_vars    = nil
  self
end