tina4ruby 3.13.99 → 3.13.100
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +18 -0
- data/lib/tina4/frond.rb +253 -25
- data/lib/tina4/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 850c8661fec2f0bb0ffeaf0c5d7644a6625b59878f5b053aeba49a4fa48cbde7
|
|
4
|
+
data.tar.gz: df0cbd73ccc67d39ac6f2900d8d2e6a9875925fbf85dfd092bdf1579e4748f38
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 39994977bdc2437b6abbfdeb88b0f3727e43cb46a4c189aae216e37202bdcd2e34505e396c60b083df3712c9bab1ac160f8f410b629e00a686dd9e9b48a4589b
|
|
7
|
+
data.tar.gz: b4948b71e5ecad37aa7923bc96091d88e243d0f3599e4a3440e94cc38a8452be2d8bb993459487590ef905752424b441376d78983597b94f4ac7059426780eae
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,24 @@ number means the same thing everywhere.
|
|
|
6
6
|
**The authoritative release notes for every shipped version live in the documentation:**
|
|
7
7
|
https://tina4.com/ruby/36-releases
|
|
8
8
|
|
|
9
|
+
## 3.13.100
|
|
10
|
+
|
|
11
|
+
### Breaking: Frond instance extensions stay local
|
|
12
|
+
|
|
13
|
+
Calling `add_filter`, `add_global`, or `add_test` on a Frond instance now changes
|
|
14
|
+
that renderer only. Register on `Frond` itself when every later instance must
|
|
15
|
+
inherit the extension.
|
|
16
|
+
|
|
17
|
+
- Reject a second `{% extends %}` tag instead of replacing the first parent without warning.
|
|
18
|
+
- Resolve multi-level inheritance without recursing through the same child template.
|
|
19
|
+
- Preserve nested root blocks through a depth-aware final substitution pass.
|
|
20
|
+
- Bound template, fragment, and expression caches, with TTL sweeps for stale entries.
|
|
21
|
+
- Retry transient AI skill-download failures.
|
|
22
|
+
- Activate the tina4-js skill for `tina4js` and `Tina4 JS` spellings as well as `tina4-js`.
|
|
23
|
+
- Keep `Tina4::VERSION` and both AI-facing guide markers on one version.
|
|
24
|
+
|
|
25
|
+
## 3.13.99
|
|
26
|
+
|
|
9
27
|
### Breaking: `request.params` is route-params-only, and `path_params` is renamed `params`
|
|
10
28
|
|
|
11
29
|
Ruby had the worst version of the param-pollution bug: `params` used to merge the query
|
data/lib/tina4/frond.rb
CHANGED
|
@@ -31,9 +31,8 @@ module Tina4
|
|
|
31
31
|
# late-constructed engines automatically inherit prior registrations.
|
|
32
32
|
#
|
|
33
33
|
# The same-name dual-callable (class + instance) methods below let callers
|
|
34
|
-
# write either ``Tina4::Frond.add_filter(...)`` (
|
|
35
|
-
# ``frond.add_filter(...)`` (
|
|
36
|
-
# instance's live filter map). Parity with tina4-python's
|
|
34
|
+
# write either ``Tina4::Frond.add_filter(...)`` (process-global) or
|
|
35
|
+
# ``frond.add_filter(...)`` (the instance's live filter map only). Parity with tina4-python's
|
|
37
36
|
# ``_ClassOrInstanceMethod`` descriptor.
|
|
38
37
|
@@class_filters = {}
|
|
39
38
|
@@class_globals = {}
|
|
@@ -85,6 +84,11 @@ module Tina4
|
|
|
85
84
|
# -- Compiled regex constants (optimization: avoid re-compiling in methods) --
|
|
86
85
|
EXTENDS_RE = /\{%-?\s*extends\s+["'](.+?)["']\s*-?%\}/
|
|
87
86
|
BLOCK_RE = /\{%-?\s*block\s+(\w+)\s*-?%\}(.*?)\{%-?\s*endblock\s*-?%\}/m
|
|
87
|
+
# Open/close halves of BLOCK_RE, used standalone by extract_blocks' depth
|
|
88
|
+
# counter -- a single non-greedy BLOCK_RE match cannot tell a NESTED
|
|
89
|
+
# endblock from the outer block's own, so it needs its own two-piece scan.
|
|
90
|
+
BLOCK_OPEN_RE = /\{%-?\s*block\s+(\w+)\s*-?%\}/
|
|
91
|
+
BLOCK_CLOSE_RE = /\{%-?\s*endblock\s*-?%\}/
|
|
88
92
|
STRING_LIT_RE = /\A["'](.*)["']\z/
|
|
89
93
|
INTEGER_RE = /\A-?\d+\z/
|
|
90
94
|
FLOAT_RE = /\A-?\d+\.\d+\z/
|
|
@@ -204,8 +208,22 @@ module Tina4
|
|
|
204
208
|
# exists for the workload that genuinely grows without limit for the life
|
|
205
209
|
# of a worker: +render_string+ keys on md5(source), so an app that builds
|
|
206
210
|
# template strings dynamically adds an entry per distinct string.
|
|
211
|
+
#
|
|
212
|
+
# Also reused for @fragment_cache (the {% cache %} tag's runtime store):
|
|
213
|
+
# a rendered fragment is a whole HTML string, the same order of magnitude
|
|
214
|
+
# as a compiled template, not a small per-expression descriptor.
|
|
207
215
|
TEMPLATE_CACHE_MAX = 256
|
|
208
216
|
|
|
217
|
+
# Hard cap on every per-expression memo cache in this engine (ADR-0004):
|
|
218
|
+
# @filter_chain_cache, @resolve_cache, @dotted_split_cache. Mirrors PHP's
|
|
219
|
+
# MEMO_CACHE_MAX and the Python master's `@lru_cache(maxsize=1024)` on the
|
|
220
|
+
# equivalent module-level parsers. A template that builds expression
|
|
221
|
+
# strings dynamically would otherwise grow a plain instance Hash without
|
|
222
|
+
# limit for the lifetime of the engine — a memory footgun on a long-lived
|
|
223
|
+
# worker. Deliberately higher than TEMPLATE_CACHE_MAX: one entry here is a
|
|
224
|
+
# small parsed-path array, orders of magnitude smaller than a token list.
|
|
225
|
+
MEMO_CACHE_MAX = 1024
|
|
226
|
+
|
|
209
227
|
# -- Lazy context overlay for for-loops (avoids full Hash#dup) --
|
|
210
228
|
class LoopContext
|
|
211
229
|
def initialize(parent)
|
|
@@ -420,31 +438,25 @@ module Tina4
|
|
|
420
438
|
|
|
421
439
|
# Register a custom filter.
|
|
422
440
|
#
|
|
423
|
-
# Updates
|
|
424
|
-
#
|
|
425
|
-
# visible to subsequent renders on the current engine).
|
|
441
|
+
# Updates this instance's live filter map only. Class calls remain the
|
|
442
|
+
# process-global registration path. tina4: ADR-0052.
|
|
426
443
|
def add_filter(name, &blk)
|
|
427
|
-
self.class.add_filter(name, &blk)
|
|
428
444
|
@filters[name.to_s] = blk
|
|
429
445
|
self
|
|
430
446
|
end
|
|
431
447
|
|
|
432
448
|
# Register a custom test.
|
|
433
449
|
#
|
|
434
|
-
# Updates
|
|
435
|
-
# See ``add_filter`` for the dual-write semantics.
|
|
450
|
+
# Updates this instance's live tests map only.
|
|
436
451
|
def add_test(name, &blk)
|
|
437
|
-
self.class.add_test(name, &blk)
|
|
438
452
|
@tests[name.to_s] = blk
|
|
439
453
|
self
|
|
440
454
|
end
|
|
441
455
|
|
|
442
456
|
# Register a global variable available in all templates.
|
|
443
457
|
#
|
|
444
|
-
# Updates
|
|
445
|
-
# See ``add_filter`` for the dual-write semantics.
|
|
458
|
+
# Updates this instance's live globals map only.
|
|
446
459
|
def add_global(name, value)
|
|
447
|
-
self.class.add_global(name, value)
|
|
448
460
|
@globals[name.to_s] = value
|
|
449
461
|
self
|
|
450
462
|
end
|
|
@@ -577,6 +589,29 @@ module Tina4
|
|
|
577
589
|
cache.keys.first(max_entries / 2).each { |key| cache.delete(key) }
|
|
578
590
|
end
|
|
579
591
|
|
|
592
|
+
# Drop every TTL-expired entry from the {% cache %} fragment store.
|
|
593
|
+
#
|
|
594
|
+
# cap_cache bounds @fragment_cache by SIZE (insertion order, oldest
|
|
595
|
+
# first) but says nothing about STALENESS: a key that expired and is
|
|
596
|
+
# never visited again would otherwise sit in the Hash, still counted
|
|
597
|
+
# against the cap, until something else finally evicts it. An app
|
|
598
|
+
# keying fragments on a dynamic value (a page id, a user id) can churn
|
|
599
|
+
# through many such keys, so staleness has to be swept on its own
|
|
600
|
+
# schedule, not just bounded by count.
|
|
601
|
+
#
|
|
602
|
+
# Called on every {% cache %} render (cheap: bounded by TEMPLATE_CACHE_MAX
|
|
603
|
+
# entries, so at most 256 comparisons) rather than only for the key being
|
|
604
|
+
# read, so an unrelated key's expiry is cleaned up as a side effect of
|
|
605
|
+
# ANY fragment-cache render, not just a future hit on that same key.
|
|
606
|
+
#
|
|
607
|
+
# @param cache [Hash] fragment cache to sweep, mutated in place —
|
|
608
|
+
# key => [html, expires_at_unix_float]
|
|
609
|
+
# @return [void]
|
|
610
|
+
def sweep_expired_cache(cache)
|
|
611
|
+
now = Time.now.to_f
|
|
612
|
+
cache.delete_if { |_key, (_html, expires_at)| expires_at <= now }
|
|
613
|
+
end
|
|
614
|
+
|
|
580
615
|
# -----------------------------------------------------------------------
|
|
581
616
|
# Tokenizer
|
|
582
617
|
# -----------------------------------------------------------------------
|
|
@@ -701,10 +736,29 @@ module Tina4
|
|
|
701
736
|
render_tokens(tokens, context)
|
|
702
737
|
end
|
|
703
738
|
|
|
739
|
+
# Return this template's OWN {% extends %} parent name, or nil.
|
|
740
|
+
#
|
|
741
|
+
# A template may extend at most one parent. Before 3.13.100 a SECOND
|
|
742
|
+
# {% extends %} tag anywhere in the source was silently invisible: only
|
|
743
|
+
# the first occurrence was ever matched, and the rest of the child's
|
|
744
|
+
# non-block content -- including the second extends tag -- was already
|
|
745
|
+
# discarded the same way ordinary non-block child content always is
|
|
746
|
+
# during inheritance. That hid what is almost always a mistake (a
|
|
747
|
+
# copy-paste, a bad merge) with zero signal. Raise clearly instead, the
|
|
748
|
+
# same policy 3.13.89 applied to an unknown tag.
|
|
749
|
+
def extends_target(source)
|
|
750
|
+
matches = source.scan(EXTENDS_RE)
|
|
751
|
+
if matches.length > 1
|
|
752
|
+
raise "Frond: template has #{matches.length} \"{% extends %}\" tags -- " \
|
|
753
|
+
"a template can extend only one parent"
|
|
754
|
+
end
|
|
755
|
+
source =~ EXTENDS_RE ? Regexp.last_match(1) : nil
|
|
756
|
+
end
|
|
757
|
+
|
|
704
758
|
def execute_with_tokens(source, tokens, context)
|
|
705
759
|
# Handle extends first
|
|
706
|
-
|
|
707
|
-
|
|
760
|
+
parent_name = extends_target(source)
|
|
761
|
+
if parent_name
|
|
708
762
|
parent_source = load_template(parent_name)
|
|
709
763
|
child_blocks = extract_blocks(source)
|
|
710
764
|
return render_with_blocks(parent_source, context, child_blocks)
|
|
@@ -715,8 +769,8 @@ module Tina4
|
|
|
715
769
|
|
|
716
770
|
def execute(source, context)
|
|
717
771
|
# Handle extends first
|
|
718
|
-
|
|
719
|
-
|
|
772
|
+
parent_name = extends_target(source)
|
|
773
|
+
if parent_name
|
|
720
774
|
parent_source = load_template(parent_name)
|
|
721
775
|
child_blocks = extract_blocks(source)
|
|
722
776
|
return render_with_blocks(parent_source, context, child_blocks)
|
|
@@ -725,20 +779,135 @@ module Tina4
|
|
|
725
779
|
render_tokens(tokenize(source), context)
|
|
726
780
|
end
|
|
727
781
|
|
|
782
|
+
# Extract {% block name %}...{% endblock %} from source, TOP-LEVEL only.
|
|
783
|
+
#
|
|
784
|
+
# Counts depth rather than relying on a single non-greedy BLOCK_RE scan:
|
|
785
|
+
# a plain scan cannot tell a NESTED block's own {% endblock %} from the
|
|
786
|
+
# outer block's real one, so it pairs the outer open with whichever
|
|
787
|
+
# {% endblock %} happens to come first -- silently truncating the outer
|
|
788
|
+
# block's captured content at the wrong tag. A nested block's own markup
|
|
789
|
+
# stays embedded, VERBATIM, inside its outer block's captured content
|
|
790
|
+
# here; resolving it is render_with_blocks' fixed-point substitution
|
|
791
|
+
# loop's job, not this method's. Matches the Python master's
|
|
792
|
+
# _extract_blocks.
|
|
728
793
|
def extract_blocks(source)
|
|
729
794
|
blocks = {}
|
|
730
|
-
|
|
731
|
-
|
|
795
|
+
pos = 0
|
|
796
|
+
len = source.length
|
|
797
|
+
|
|
798
|
+
while pos < len
|
|
799
|
+
m_open = BLOCK_OPEN_RE.match(source, pos)
|
|
800
|
+
break unless m_open
|
|
801
|
+
|
|
802
|
+
name = m_open[1]
|
|
803
|
+
content_start = m_open.end(0)
|
|
804
|
+
depth = 1
|
|
805
|
+
scan = content_start
|
|
806
|
+
matched = false
|
|
807
|
+
|
|
808
|
+
while depth.positive? && scan < len
|
|
809
|
+
next_open = BLOCK_OPEN_RE.match(source, scan)
|
|
810
|
+
next_close = BLOCK_CLOSE_RE.match(source, scan)
|
|
811
|
+
break if next_close.nil? # malformed -- no matching endblock
|
|
812
|
+
|
|
813
|
+
if next_open && next_open.begin(0) < next_close.begin(0)
|
|
814
|
+
depth += 1
|
|
815
|
+
scan = next_open.end(0)
|
|
816
|
+
else
|
|
817
|
+
depth -= 1
|
|
818
|
+
if depth.zero?
|
|
819
|
+
blocks[name] = source[content_start...next_close.begin(0)]
|
|
820
|
+
pos = next_close.end(0)
|
|
821
|
+
matched = true
|
|
822
|
+
break
|
|
823
|
+
end
|
|
824
|
+
scan = next_close.end(0)
|
|
825
|
+
end
|
|
826
|
+
end
|
|
827
|
+
|
|
828
|
+
pos = content_start unless matched # malformed -- skip forward
|
|
732
829
|
end
|
|
830
|
+
|
|
733
831
|
blocks
|
|
734
832
|
end
|
|
735
833
|
|
|
736
|
-
|
|
834
|
+
# Depth-aware block substitution against `source` (typically the
|
|
835
|
+
# fully-resolved root template).
|
|
836
|
+
#
|
|
837
|
+
# A single regex #gsub pass with BLOCK_RE (non-greedy) pairs an OUTER
|
|
838
|
+
# block's open tag with the FIRST {% endblock %} found -- which, when
|
|
839
|
+
# the outer block wraps a NESTED {% block %}, is the nested block's
|
|
840
|
+
# own close tag, not the outer's. That silently truncates the outer
|
|
841
|
+
# block's captured content and drops everything after the inner
|
|
842
|
+
# endblock (the root-nested-block content-loss bug: {% block body
|
|
843
|
+
# %}<section>{% block inner %}{% endblock %}</section>{% endblock %}
|
|
844
|
+
# rendered "<section></section>", the leaf's "inner" override AND
|
|
845
|
+
# root's own "body" wrapper both silently lost). This scans with an
|
|
846
|
+
# open/close depth counter instead (mirroring extract_blocks), so an
|
|
847
|
+
# outer block always captures its FULL body, nested child blocks
|
|
848
|
+
# included.
|
|
849
|
+
#
|
|
850
|
+
# The content chosen for each block -- the child override in `blocks`
|
|
851
|
+
# if present, else the block's own default body -- is then
|
|
852
|
+
# recursively substituted against the SAME `blocks` map before being
|
|
853
|
+
# tokenized and rendered, so a block nested inside another block
|
|
854
|
+
# resolves correctly regardless of which template in the inheritance
|
|
855
|
+
# chain declared the nesting (the root, an intermediate, however many
|
|
856
|
+
# levels deep).
|
|
857
|
+
#
|
|
858
|
+
# {{ parent() }} / {{ super() }} inside a block still render that
|
|
859
|
+
# block's OWN default content at this level (lazy, on first call).
|
|
860
|
+
def substitute_blocks(source, blocks, context)
|
|
737
861
|
engine = self
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
862
|
+
len = source.length
|
|
863
|
+
pieces = []
|
|
864
|
+
pos = 0
|
|
865
|
+
|
|
866
|
+
while pos < len
|
|
867
|
+
m_open = BLOCK_OPEN_RE.match(source, pos)
|
|
868
|
+
unless m_open
|
|
869
|
+
pieces << source[pos..]
|
|
870
|
+
break
|
|
871
|
+
end
|
|
872
|
+
|
|
873
|
+
pieces << source[pos...m_open.begin(0)] # untouched text before the tag
|
|
874
|
+
|
|
875
|
+
name = m_open[1]
|
|
876
|
+
content_start = m_open.end(0)
|
|
877
|
+
depth = 1
|
|
878
|
+
scan = content_start
|
|
879
|
+
close_match = nil
|
|
880
|
+
|
|
881
|
+
while depth.positive? && scan < len
|
|
882
|
+
next_open = BLOCK_OPEN_RE.match(source, scan)
|
|
883
|
+
next_close = BLOCK_CLOSE_RE.match(source, scan)
|
|
884
|
+
break if next_close.nil? # malformed -- no matching endblock
|
|
885
|
+
|
|
886
|
+
if next_open && next_open.begin(0) < next_close.begin(0)
|
|
887
|
+
depth += 1
|
|
888
|
+
scan = next_open.end(0)
|
|
889
|
+
else
|
|
890
|
+
depth -= 1
|
|
891
|
+
if depth.zero?
|
|
892
|
+
close_match = next_close
|
|
893
|
+
else
|
|
894
|
+
scan = next_close.end(0)
|
|
895
|
+
end
|
|
896
|
+
end
|
|
897
|
+
end
|
|
898
|
+
|
|
899
|
+
if close_match.nil?
|
|
900
|
+
# Malformed template (no matching endblock) -- keep the rest
|
|
901
|
+
# verbatim rather than lose it, the same leniency extract_blocks
|
|
902
|
+
# applies to this case.
|
|
903
|
+
pieces << source[m_open.begin(0)..]
|
|
904
|
+
pos = len
|
|
905
|
+
break
|
|
906
|
+
end
|
|
907
|
+
|
|
908
|
+
parent_content = source[content_start...close_match.begin(0)]
|
|
909
|
+
block_source = blocks.fetch(name, parent_content)
|
|
910
|
+
resolved_source = substitute_blocks(block_source, blocks, context)
|
|
742
911
|
|
|
743
912
|
# Make parent() and super() available inside child blocks
|
|
744
913
|
rendered_parent = nil
|
|
@@ -750,8 +919,61 @@ module Tina4
|
|
|
750
919
|
end
|
|
751
920
|
|
|
752
921
|
block_ctx = context.merge("parent" => get_parent, "super" => get_parent)
|
|
753
|
-
render_tokens(tokenize(
|
|
922
|
+
pieces << render_tokens(tokenize(resolved_source), block_ctx)
|
|
923
|
+
pos = close_match.end(0)
|
|
924
|
+
end
|
|
925
|
+
|
|
926
|
+
pieces.join
|
|
927
|
+
end
|
|
928
|
+
|
|
929
|
+
def render_with_blocks(parent_source, context, child_blocks)
|
|
930
|
+
# Multi-level extends: when this parent ITSELF has its own {% extends %},
|
|
931
|
+
# merge its own block defaults under the child's overrides (the nearer
|
|
932
|
+
# descendant always wins) and recurse until a root template with no
|
|
933
|
+
# {% extends %} is reached. Before 3.13.100 this recursion never
|
|
934
|
+
# happened: a mid-level parent's own {% extends %} tag reached
|
|
935
|
+
# render_tokens' "block/endblock/extends" case, which is a silent
|
|
936
|
+
# no-op, so a 3+ level chain lost the root's wrapping entirely and any
|
|
937
|
+
# of the mid template's own non-block text leaked through unwrapped
|
|
938
|
+
# instead. Matches the Python/PHP/Node masters, which already recurse
|
|
939
|
+
# the parent -> grandparent chain the same way.
|
|
940
|
+
grandparent_name = extends_target(parent_source)
|
|
941
|
+
if grandparent_name
|
|
942
|
+
parent_blocks = extract_blocks(parent_source)
|
|
943
|
+
merged_blocks = parent_blocks.merge(child_blocks)
|
|
944
|
+
|
|
945
|
+
# Resolve NESTED blocks: a block value that itself contains a
|
|
946
|
+
# {% block inner %}...{% endblock %} tag (this level's own markup,
|
|
947
|
+
# not yet substituted) has that inner tag replaced with the merged
|
|
948
|
+
# dict's value for that name, or the inner tag's own default when
|
|
949
|
+
# nothing overrides it. Fixed-point because resolving one level can
|
|
950
|
+
# reveal another (a block three levels deep). Matches Python/Node's
|
|
951
|
+
# multi-level extends, and is what lets a grandchild override a
|
|
952
|
+
# block nested INSIDE a block the middle template redeclares.
|
|
953
|
+
changed = true
|
|
954
|
+
while changed
|
|
955
|
+
changed = false
|
|
956
|
+
merged_blocks.keys.each do |name|
|
|
957
|
+
resolved = merged_blocks[name].gsub(BLOCK_RE) do
|
|
958
|
+
inner_name = Regexp.last_match(1)
|
|
959
|
+
inner_default = Regexp.last_match(2)
|
|
960
|
+
merged_blocks.fetch(inner_name, inner_default)
|
|
961
|
+
end
|
|
962
|
+
if resolved != merged_blocks[name]
|
|
963
|
+
merged_blocks[name] = resolved
|
|
964
|
+
changed = true
|
|
965
|
+
end
|
|
966
|
+
end
|
|
967
|
+
end
|
|
968
|
+
|
|
969
|
+
grandparent_source = load_template(grandparent_name)
|
|
970
|
+
return render_with_blocks(grandparent_source, context, merged_blocks)
|
|
754
971
|
end
|
|
972
|
+
|
|
973
|
+
# Depth-aware block substitution (handles a block nested inside
|
|
974
|
+
# another block at ANY level of the chain, including the root
|
|
975
|
+
# itself -- see substitute_blocks).
|
|
976
|
+
result = substitute_blocks(parent_source, child_blocks, context)
|
|
755
977
|
render_tokens(tokenize(result), context)
|
|
756
978
|
end
|
|
757
979
|
|
|
@@ -1015,6 +1237,7 @@ module Tina4
|
|
|
1015
1237
|
root_var = @dotted_split_cache[var_name]
|
|
1016
1238
|
unless root_var
|
|
1017
1239
|
root_var = var_name.split(".")[0].split("[")[0].strip
|
|
1240
|
+
cap_cache(@dotted_split_cache, MEMO_CACHE_MAX)
|
|
1018
1241
|
@dotted_split_cache[var_name] = root_var
|
|
1019
1242
|
end
|
|
1020
1243
|
return "" if !root_var.empty? && !@allowed_vars.include?(root_var) && root_var != "loop"
|
|
@@ -1260,6 +1483,7 @@ module Tina4
|
|
|
1260
1483
|
end
|
|
1261
1484
|
|
|
1262
1485
|
result = [variable, filters].freeze
|
|
1486
|
+
cap_cache(@filter_chain_cache, MEMO_CACHE_MAX)
|
|
1263
1487
|
@filter_chain_cache[expr] = result
|
|
1264
1488
|
result
|
|
1265
1489
|
end
|
|
@@ -1784,6 +2008,7 @@ module Tina4
|
|
|
1784
2008
|
parts = @resolve_cache[expr]
|
|
1785
2009
|
unless parts
|
|
1786
2010
|
parts = expr.split(RESOLVE_SPLIT_RE).reject(&:empty?)
|
|
2011
|
+
cap_cache(@resolve_cache, MEMO_CACHE_MAX)
|
|
1787
2012
|
@resolve_cache[expr] = parts
|
|
1788
2013
|
end
|
|
1789
2014
|
|
|
@@ -2265,6 +2490,8 @@ module Tina4
|
|
|
2265
2490
|
cache_key = m ? m[1] : "default"
|
|
2266
2491
|
ttl = m && m[2] ? m[2].to_i : 60
|
|
2267
2492
|
|
|
2493
|
+
sweep_expired_cache(@fragment_cache)
|
|
2494
|
+
|
|
2268
2495
|
# Check cache
|
|
2269
2496
|
cached = @fragment_cache[cache_key]
|
|
2270
2497
|
if cached
|
|
@@ -2317,6 +2544,7 @@ module Tina4
|
|
|
2317
2544
|
end
|
|
2318
2545
|
|
|
2319
2546
|
rendered = render_tokens(body_tokens.dup, context)
|
|
2547
|
+
cap_cache(@fragment_cache, TEMPLATE_CACHE_MAX)
|
|
2320
2548
|
@fragment_cache[cache_key] = [rendered, Time.now.to_f + ttl]
|
|
2321
2549
|
[rendered, i]
|
|
2322
2550
|
end
|
data/lib/tina4/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: tina4ruby
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.13.
|
|
4
|
+
version: 3.13.100
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Tina4 Team
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-08-
|
|
11
|
+
date: 2026-08-14 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rack
|