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.



1971
1972
1973
# File 'lib/tina4/frond.rb', line 1971

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



276
277
278
# File 'lib/tina4/frond.rb', line 276

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.



1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
# File 'lib/tina4/frond.rb', line 1990

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



2014
2015
2016
2017
# File 'lib/tina4/frond.rb', line 2014

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.



2021
2022
2023
# File 'lib/tina4/frond.rb', line 2021

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.



1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
# File 'lib/tina4/frond.rb', line 1946

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



1977
1978
1979
# File 'lib/tina4/frond.rb', line 1977

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.



243
244
245
# File 'lib/tina4/frond.rb', line 243

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

#add_global(name, value) ⇒ Object

Register a global variable available in all templates.



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

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

#add_test(name, &blk) ⇒ Object

Register a custom test.



248
249
250
# File 'lib/tina4/frond.rb', line 248

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

#clear_cacheObject

Clear all compiled template caches.



234
235
236
237
238
239
240
# File 'lib/tina4/frond.rb', line 234

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.



193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
# File 'lib/tina4/frond.rb', line 193

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"

  unless debug_mode
    # Production: use permanent cache (no filesystem checks)
    cached = @compiled[template]
    return execute_cached(cached[0], context) if cached
  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]
  execute_with_tokens(source, tokens, context)
end

#render_string(source, data = {}) ⇒ Object

Render a template string directly. Uses token caching for performance.



218
219
220
221
222
223
224
225
226
227
228
229
230
231
# File 'lib/tina4/frond.rb', line 218

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.



258
259
260
261
262
263
264
# File 'lib/tina4/frond.rb', line 258

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.



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

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