tina4ruby 3.13.87 → 3.13.89
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/lib/tina4/frond.rb +152 -14
- data/lib/tina4/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c9a4969bc3af1b2a6f203d215043c4fb9497aa40e71a0f7d1c17d92eddeecea3
|
|
4
|
+
data.tar.gz: 67a4a34e48ca3e167201ab06ac5cab96be36b4f4224a97f04cae0c58f700184c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a89938265a10f5b8f0a48966f370c55006937a236285fe6b8f2fd9c5f75464d5c4fc0fc65cf1583f76539e24cd095701e3c092d87fb9d1197978d1d9774f552c
|
|
7
|
+
data.tar.gz: af228d8eebfab34598468e44f6962ac701280d4ad1207e4184f2eee6aabc631c5dda36753de7c99b1e8aea4dde7cff635fa2c44c30fff469532124cab0d9f11a
|
data/lib/tina4/frond.rb
CHANGED
|
@@ -143,6 +143,26 @@ module Tina4
|
|
|
143
143
|
IMPORT_AS_RE = /\Aimport\s+["'](.+?)["']\s+as\s+(\w+)/
|
|
144
144
|
CACHE_RE = /\Acache\s+["'](.+?)["']\s*(\d+)?/
|
|
145
145
|
SPACELESS_RE = />\s+</
|
|
146
|
+
|
|
147
|
+
# Every tag that OPENS a construct. An unknown tag is a typo, and 3.13.89
|
|
148
|
+
# makes it raise rather than render its body: a mistyped guard --
|
|
149
|
+
# {% iff is_admin %} instead of {% if is_admin %} -- used to render the gated
|
|
150
|
+
# content UNCONDITIONALLY, so a reviewer saw a guard that was not there. Twig
|
|
151
|
+
# and Jinja2 both raise on an unknown tag; Frond now does too. There is no
|
|
152
|
+
# user-extension point for tags in any of the four frameworks, so an unknown
|
|
153
|
+
# name is always a mistake, never a plugin.
|
|
154
|
+
KNOWN_TAGS = %w[
|
|
155
|
+
autoescape block cache extends for from if import include live macro raw set
|
|
156
|
+
spaceless
|
|
157
|
+
].freeze
|
|
158
|
+
|
|
159
|
+
# Terminators and branch keywords. These reach the tag dispatch only when
|
|
160
|
+
# stray (their own collector consumes them in the normal case), and a stray
|
|
161
|
+
# one keeps the old render-nothing behaviour -- see the comment at the raise.
|
|
162
|
+
TERMINATOR_TAGS = %w[
|
|
163
|
+
elif else elseif endautoescape endblock endcache endfor endif endlive
|
|
164
|
+
endmacro endraw endset endspaceless
|
|
165
|
+
].freeze
|
|
146
166
|
AUTOESCAPE_RE = /\Aautoescape\s+(false|true)/
|
|
147
167
|
STRIPTAGS_RE = /<[^>]+>/
|
|
148
168
|
THOUSANDS_RE = /(\d)(?=(\d{3})+(?!\d))/
|
|
@@ -429,6 +449,66 @@ module Tina4
|
|
|
429
449
|
str.to_s.gsub(HTML_ESCAPE_RE, HTML_ESCAPE_MAP)
|
|
430
450
|
end
|
|
431
451
|
|
|
452
|
+
# Serializes a value to compact JSON text that is always valid JSON.
|
|
453
|
+
#
|
|
454
|
+
# Never raises and never returns an empty string: a non-finite float becomes
|
|
455
|
+
# null (the JSON spec has no Infinity or NaN) and malformed UTF-8 is scrubbed,
|
|
456
|
+
# so a payload always arrives, in the worst case as null.
|
|
457
|
+
def self.json_text(value)
|
|
458
|
+
JSON.generate(value)
|
|
459
|
+
rescue StandardError
|
|
460
|
+
# Only reached when the happy path raised, so a well-formed payload never
|
|
461
|
+
# pays for the walk.
|
|
462
|
+
begin
|
|
463
|
+
JSON.generate(json_sanitize(value))
|
|
464
|
+
rescue StandardError
|
|
465
|
+
"null"
|
|
466
|
+
end
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
# Replaces anything JSON.generate refuses with a JSON-representable stand-in.
|
|
470
|
+
def self.json_sanitize(value)
|
|
471
|
+
case value
|
|
472
|
+
when Float then value.finite? ? value : nil
|
|
473
|
+
when Hash then value.transform_values { |item| json_sanitize(item) }
|
|
474
|
+
when Array then value.map { |item| json_sanitize(item) }
|
|
475
|
+
when String then value.valid_encoding? ? value : value.scrub
|
|
476
|
+
else value
|
|
477
|
+
end
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
# Serializes to JSON that is valid JSON, valid JavaScript, and safe in HTML.
|
|
481
|
+
#
|
|
482
|
+
# THE cross-framework contract for json_encode / to_json / tojson. Keep the
|
|
483
|
+
# four implementations byte-identical; frond_expression_corpus.txt locks it.
|
|
484
|
+
#
|
|
485
|
+
# Three things this must never do, each of which was a real bug:
|
|
486
|
+
#
|
|
487
|
+
# 1. Never emit a non-finite literal. JSON.generate raises on Infinity, and
|
|
488
|
+
# the old `rescue v.to_s` turned that raise into Ruby inspect output --
|
|
489
|
+
# `{"a" => 1.0}` -- which no JSON.parse will read. Reported as
|
|
490
|
+
# tina4-php#184 by justin-k-bruce, who hit the same class of bug in PHP.
|
|
491
|
+
# 2. Never emit nothing, and never emit something that still parses and means
|
|
492
|
+
# something else. "var ROWS = ;" is at least a loud SyntaxError.
|
|
493
|
+
# 3. Never HTML-escape it. Entity-encoding JSON produces {"a":1},
|
|
494
|
+
# a SyntaxError inside <script>, which is the filter's whole point. Escape
|
|
495
|
+
# only the dangerous characters, as JSON \uXXXX escapes: the result stays
|
|
496
|
+
# valid JSON AND valid JavaScript, </script> cannot terminate the block,
|
|
497
|
+
# and it is safe inside a single-quoted attribute. This is what Jinja2's
|
|
498
|
+
# tojson does, and it is why the result is a SafeString.
|
|
499
|
+
#
|
|
500
|
+
# U+2028 and U+2029 join that escape set. Both are legal inside a JSON string
|
|
501
|
+
# and both were illegal inside a JavaScript string literal before ES2019.
|
|
502
|
+
def self.json_safe(value)
|
|
503
|
+
Tina4::SafeString.new(json_text(value).gsub(JSON_ESCAPE_RE, JSON_ESCAPE_MAP))
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
JSON_ESCAPE_MAP = {
|
|
507
|
+
"<" => "\\u003c", ">" => "\\u003e", "&" => "\\u0026", "'" => "\\u0027",
|
|
508
|
+
"\u2028" => "\\u2028", "\u2029" => "\\u2029"
|
|
509
|
+
}.freeze
|
|
510
|
+
JSON_ESCAPE_RE = /[<>&'\u2028\u2029]/
|
|
511
|
+
|
|
432
512
|
private
|
|
433
513
|
|
|
434
514
|
# Keep a memo cache bounded (ADR-0004). Call immediately before inserting
|
|
@@ -653,8 +733,17 @@ module Tina4
|
|
|
653
733
|
result, i = handle_for(tokens, i, context)
|
|
654
734
|
output << result
|
|
655
735
|
when "set"
|
|
656
|
-
|
|
657
|
-
|
|
736
|
+
# An assignment has an "="; without one this is the BLOCK form,
|
|
737
|
+
# {% set name %}...{% endset %}, which captures its rendered body.
|
|
738
|
+
# A bare include? is exact here, not a shortcut: the block form's tag
|
|
739
|
+
# content is only ever "set <name>", so an "=" anywhere -- even inside
|
|
740
|
+
# a quoted value like {% set m = "a = b" %} -- means assignment.
|
|
741
|
+
if content.include?("=")
|
|
742
|
+
handle_set(content, context)
|
|
743
|
+
i += 1
|
|
744
|
+
else
|
|
745
|
+
i = handle_set_block(tokens, i, context)
|
|
746
|
+
end
|
|
658
747
|
when "include"
|
|
659
748
|
if @sandbox && @allowed_tags && !@allowed_tags.include?("include")
|
|
660
749
|
i += 1
|
|
@@ -686,6 +775,15 @@ module Tina4
|
|
|
686
775
|
i += 1
|
|
687
776
|
else
|
|
688
777
|
i += 1
|
|
778
|
+
unless tag.empty? || TERMINATOR_TAGS.include?(tag)
|
|
779
|
+
raise ArgumentError,
|
|
780
|
+
%(Frond: unknown tag "#{tag}" -- known tags are: #{KNOWN_TAGS.sort.join(", ")})
|
|
781
|
+
end
|
|
782
|
+
# An empty tag ({% %}) or a stray terminator (an {% endif %} with
|
|
783
|
+
# no {% if %}): no output.
|
|
784
|
+
# Malformed, but it has always rendered nothing, and nothing is the
|
|
785
|
+
# safe answer -- unlike an unknown tag it cannot expose content that
|
|
786
|
+
# was meant to be gated.
|
|
689
787
|
end
|
|
690
788
|
|
|
691
789
|
if strip_a && i < tokens.length && tokens[i][0] == TEXT
|
|
@@ -2284,6 +2382,48 @@ module Tina4
|
|
|
2284
2382
|
)
|
|
2285
2383
|
end
|
|
2286
2384
|
|
|
2385
|
+
# {% set name %}...{% endset %} -- render the body and bind it.
|
|
2386
|
+
#
|
|
2387
|
+
# Emits nothing itself. The captured value is a SafeString because it is
|
|
2388
|
+
# template output that has already been escaped on the way in; re-escaping it
|
|
2389
|
+
# at {{ name }} would double-encode every entity. Twig and Jinja2 both mark
|
|
2390
|
+
# the capture safe. Returns the index just past {% endset %}.
|
|
2391
|
+
def handle_set_block(tokens, start, context)
|
|
2392
|
+
content, _, _ = strip_tag(tokens[start][1])
|
|
2393
|
+
name = (content.split[1] || "").strip
|
|
2394
|
+
|
|
2395
|
+
body_tokens = []
|
|
2396
|
+
i = start + 1
|
|
2397
|
+
depth = 0
|
|
2398
|
+
while i < tokens.length
|
|
2399
|
+
if tokens[i][0] == BLOCK
|
|
2400
|
+
tc, _, _ = strip_tag(tokens[i][1])
|
|
2401
|
+
tag = tc.split[0] || ""
|
|
2402
|
+
if tag == "set" && !tc.include?("=")
|
|
2403
|
+
depth += 1
|
|
2404
|
+
body_tokens << tokens[i]
|
|
2405
|
+
elsif tag == "endset"
|
|
2406
|
+
if depth.zero?
|
|
2407
|
+
i += 1
|
|
2408
|
+
break
|
|
2409
|
+
end
|
|
2410
|
+
depth -= 1
|
|
2411
|
+
body_tokens << tokens[i]
|
|
2412
|
+
else
|
|
2413
|
+
body_tokens << tokens[i]
|
|
2414
|
+
end
|
|
2415
|
+
else
|
|
2416
|
+
body_tokens << tokens[i]
|
|
2417
|
+
end
|
|
2418
|
+
i += 1
|
|
2419
|
+
end
|
|
2420
|
+
|
|
2421
|
+
unless name.empty?
|
|
2422
|
+
context[name] = Tina4::SafeString.new(render_tokens(body_tokens.dup, context))
|
|
2423
|
+
end
|
|
2424
|
+
i
|
|
2425
|
+
end
|
|
2426
|
+
|
|
2287
2427
|
def handle_spaceless(tokens, start, context)
|
|
2288
2428
|
body_tokens = []
|
|
2289
2429
|
i = start + 1
|
|
@@ -2406,7 +2546,10 @@ module Tina4
|
|
|
2406
2546
|
"e" => ->(v, *_a) { Tina4::SafeString.new(Frond.escape_html(v.to_s)) },
|
|
2407
2547
|
"raw" => ->(v, *_a) { v },
|
|
2408
2548
|
"safe" => ->(v, *_a) { v },
|
|
2409
|
-
|
|
2549
|
+
# Frond.json_safe, never a bare JSON.generate: generate RAISES on an
|
|
2550
|
+
# Infinity/NaN float, and the old `rescue v.to_s` answered that raise with
|
|
2551
|
+
# Ruby inspect output that no JSON.parse will read. See Frond.json_safe.
|
|
2552
|
+
"json_encode" => ->(v, *_a) { Frond.json_safe(v) },
|
|
2410
2553
|
"json_decode" => ->(v, *_a) { v.is_a?(String) ? (JSON.parse(v) rescue v) : v },
|
|
2411
2554
|
"base64_encode" => ->(v, *_a) { Base64.strict_encode64(v.is_a?(String) ? v : v.to_s) },
|
|
2412
2555
|
"base64encode" => ->(v, *_a) { Base64.strict_encode64(v.is_a?(String) ? v : v.to_s) },
|
|
@@ -2425,17 +2568,12 @@ module Tina4
|
|
|
2425
2568
|
"url_encode" => ->(v, *_a) { ERB::Util.url_encode(v.to_s) },
|
|
2426
2569
|
|
|
2427
2570
|
# -- JSON / JS --
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
},
|
|
2434
|
-
"tojson" => ->(v, *a) {
|
|
2435
|
-
indent = a[0] ? a[0].to_i : nil
|
|
2436
|
-
json = indent ? JSON.pretty_generate(v) : JSON.generate(v)
|
|
2437
|
-
Tina4::SafeString.new(json.gsub("<", '\u003c').gsub(">", '\u003e').gsub("&", '\u0026'))
|
|
2438
|
-
},
|
|
2571
|
+
# Same serializer as json_encode -- the three names are one behaviour.
|
|
2572
|
+
# The old indent argument is gone: PHP cannot honour an arbitrary indent
|
|
2573
|
+
# (JSON_PRETTY_PRINT is fixed at four spaces), so honouring it here alone
|
|
2574
|
+
# broke byte-parity for the one filter whose whole job is a wire format.
|
|
2575
|
+
"to_json" => ->(v, *_a) { Frond.json_safe(v) },
|
|
2576
|
+
"tojson" => ->(v, *_a) { Frond.json_safe(v) },
|
|
2439
2577
|
"js_escape" => ->(v, *_a) {
|
|
2440
2578
|
Tina4::SafeString.new(
|
|
2441
2579
|
v.to_s.gsub("\\", "\\\\").gsub("'", "\\'").gsub('"', '\\"')
|
data/lib/tina4/version.rb
CHANGED