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.



2033
2034
2035
# File 'lib/tina4/frond.rb', line 2033

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.



2052
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
2064
2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
# File 'lib/tina4/frond.rb', line 2052

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



2076
2077
2078
2079
# File 'lib/tina4/frond.rb', line 2076

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.



2083
2084
2085
# File 'lib/tina4/frond.rb', line 2083

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.



2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
# File 'lib/tina4/frond.rb', line 2008

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



2039
2040
2041
# File 'lib/tina4/frond.rb', line 2039

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