liquid2 0.1.0
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 +7 -0
- checksums.yaml.gz.sig +0 -0
- data/.rubocop.yml +46 -0
- data/.ruby-version +1 -0
- data/.vscode/settings.json +32 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/LICENSE_SHOPIFY.txt +20 -0
- data/README.md +219 -0
- data/Rakefile +23 -0
- data/Steepfile +26 -0
- data/lib/liquid2/context.rb +297 -0
- data/lib/liquid2/environment.rb +287 -0
- data/lib/liquid2/errors.rb +79 -0
- data/lib/liquid2/expression.rb +20 -0
- data/lib/liquid2/expressions/arguments.rb +25 -0
- data/lib/liquid2/expressions/array.rb +20 -0
- data/lib/liquid2/expressions/blank.rb +41 -0
- data/lib/liquid2/expressions/boolean.rb +20 -0
- data/lib/liquid2/expressions/filtered.rb +136 -0
- data/lib/liquid2/expressions/identifier.rb +43 -0
- data/lib/liquid2/expressions/lambda.rb +53 -0
- data/lib/liquid2/expressions/logical.rb +71 -0
- data/lib/liquid2/expressions/loop.rb +79 -0
- data/lib/liquid2/expressions/path.rb +33 -0
- data/lib/liquid2/expressions/range.rb +28 -0
- data/lib/liquid2/expressions/relational.rb +119 -0
- data/lib/liquid2/expressions/template_string.rb +20 -0
- data/lib/liquid2/filter.rb +95 -0
- data/lib/liquid2/filters/array.rb +202 -0
- data/lib/liquid2/filters/date.rb +20 -0
- data/lib/liquid2/filters/default.rb +16 -0
- data/lib/liquid2/filters/json.rb +15 -0
- data/lib/liquid2/filters/math.rb +87 -0
- data/lib/liquid2/filters/size.rb +11 -0
- data/lib/liquid2/filters/slice.rb +17 -0
- data/lib/liquid2/filters/sort.rb +96 -0
- data/lib/liquid2/filters/string.rb +204 -0
- data/lib/liquid2/loader.rb +59 -0
- data/lib/liquid2/loaders/file_system_loader.rb +76 -0
- data/lib/liquid2/loaders/mixins.rb +52 -0
- data/lib/liquid2/node.rb +113 -0
- data/lib/liquid2/nodes/comment.rb +18 -0
- data/lib/liquid2/nodes/output.rb +24 -0
- data/lib/liquid2/nodes/tags/assign.rb +35 -0
- data/lib/liquid2/nodes/tags/block_comment.rb +26 -0
- data/lib/liquid2/nodes/tags/capture.rb +40 -0
- data/lib/liquid2/nodes/tags/case.rb +111 -0
- data/lib/liquid2/nodes/tags/cycle.rb +63 -0
- data/lib/liquid2/nodes/tags/decrement.rb +29 -0
- data/lib/liquid2/nodes/tags/doc.rb +24 -0
- data/lib/liquid2/nodes/tags/echo.rb +31 -0
- data/lib/liquid2/nodes/tags/extends.rb +3 -0
- data/lib/liquid2/nodes/tags/for.rb +155 -0
- data/lib/liquid2/nodes/tags/if.rb +84 -0
- data/lib/liquid2/nodes/tags/include.rb +123 -0
- data/lib/liquid2/nodes/tags/increment.rb +29 -0
- data/lib/liquid2/nodes/tags/inline_comment.rb +28 -0
- data/lib/liquid2/nodes/tags/liquid.rb +29 -0
- data/lib/liquid2/nodes/tags/macro.rb +3 -0
- data/lib/liquid2/nodes/tags/raw.rb +30 -0
- data/lib/liquid2/nodes/tags/render.rb +137 -0
- data/lib/liquid2/nodes/tags/tablerow.rb +143 -0
- data/lib/liquid2/nodes/tags/translate.rb +3 -0
- data/lib/liquid2/nodes/tags/unless.rb +23 -0
- data/lib/liquid2/nodes/tags/with.rb +3 -0
- data/lib/liquid2/parser.rb +917 -0
- data/lib/liquid2/scanner.rb +595 -0
- data/lib/liquid2/static_analysis.rb +301 -0
- data/lib/liquid2/tag.rb +22 -0
- data/lib/liquid2/template.rb +182 -0
- data/lib/liquid2/undefined.rb +131 -0
- data/lib/liquid2/utils/cache.rb +80 -0
- data/lib/liquid2/utils/chain_hash.rb +40 -0
- data/lib/liquid2/utils/unescape.rb +119 -0
- data/lib/liquid2/version.rb +5 -0
- data/lib/liquid2.rb +90 -0
- data/performance/benchmark.rb +73 -0
- data/performance/memory_profile.rb +62 -0
- data/performance/profile.rb +71 -0
- data/sig/liquid2.rbs +2348 -0
- data.tar.gz.sig +0 -0
- metadata +164 -0
- metadata.gz.sig +0 -0
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Liquid2
|
4
|
+
# Combine multiple hashes for sequential lookup.
|
5
|
+
class ReadOnlyChainHash
|
6
|
+
# @param hashes
|
7
|
+
def initialize(*hashes)
|
8
|
+
@hashes = hashes.to_a
|
9
|
+
end
|
10
|
+
|
11
|
+
def [](key)
|
12
|
+
index = @hashes.length - 1
|
13
|
+
while index >= 0
|
14
|
+
h = @hashes[index]
|
15
|
+
index -= 1
|
16
|
+
return h[key] if h.key?(key)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def key?(key)
|
21
|
+
!@hashes.rindex { |h| h.key?(key) }.nil?
|
22
|
+
end
|
23
|
+
|
24
|
+
def fetch(key, default = :undefined)
|
25
|
+
index = @hashes.length - 1
|
26
|
+
while index >= 0
|
27
|
+
h = @hashes[index]
|
28
|
+
index -= 1
|
29
|
+
return h[key] if h.key?(key)
|
30
|
+
end
|
31
|
+
|
32
|
+
default
|
33
|
+
end
|
34
|
+
|
35
|
+
def size = @hashes.length
|
36
|
+
def push(hash) = @hashes << hash
|
37
|
+
alias << push
|
38
|
+
def pop = @hashes.pop
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Liquid2 # :nodoc:
|
4
|
+
# Replace escape sequences with their equivalent Unicode code point.
|
5
|
+
# This is a bit like Ruby's String#undump, but assumes surrounding quotes have been removed
|
6
|
+
# and follows JSON escaping semantics.
|
7
|
+
# @param value [String]
|
8
|
+
# @param quote [String] one of '"' or "'".
|
9
|
+
# @param token [Token]
|
10
|
+
# @return [String] A new string without escape sequences.
|
11
|
+
def self.unescape_string(value, quote, token)
|
12
|
+
unescaped = String.new(encoding: "UTF-8")
|
13
|
+
index = 0
|
14
|
+
length = value.length
|
15
|
+
|
16
|
+
while index < length
|
17
|
+
ch = value[index] || raise
|
18
|
+
if ch == "\\"
|
19
|
+
index += 1
|
20
|
+
case value[index]
|
21
|
+
when quote
|
22
|
+
unescaped << quote
|
23
|
+
when "\\"
|
24
|
+
unescaped << "\\"
|
25
|
+
when "/"
|
26
|
+
unescaped << "/"
|
27
|
+
when "b"
|
28
|
+
unescaped << "\x08"
|
29
|
+
when "f"
|
30
|
+
unescaped << "\x0C"
|
31
|
+
when "n"
|
32
|
+
unescaped << "\n"
|
33
|
+
when "r"
|
34
|
+
unescaped << "\r"
|
35
|
+
when "t"
|
36
|
+
unescaped << "\t"
|
37
|
+
when "u"
|
38
|
+
code_point, index = Liquid2.decode_hex_char(value, index, token)
|
39
|
+
unescaped << Liquid2.code_point_to_string(code_point, token)
|
40
|
+
when "$"
|
41
|
+
unescaped << "$"
|
42
|
+
else
|
43
|
+
raise LiquidSyntaxError.new("unknown escape sequence", token)
|
44
|
+
end
|
45
|
+
else
|
46
|
+
# raise LiquidSyntaxError.new("invalid character #{ch.inspect}", token) if ch.ord <= 0x1F
|
47
|
+
|
48
|
+
unescaped << ch
|
49
|
+
end
|
50
|
+
|
51
|
+
index += 1
|
52
|
+
|
53
|
+
end
|
54
|
+
|
55
|
+
unescaped
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.decode_hex_char(value, index, token)
|
59
|
+
length = value.length
|
60
|
+
|
61
|
+
raise LiquidSyntaxError.new("incomplete escape sequence", token) if index + 4 >= length
|
62
|
+
|
63
|
+
index += 1 # move past 'u'
|
64
|
+
code_point = parse_hex_digits(value[index, 4] || raise, token)
|
65
|
+
|
66
|
+
raise LiquidSyntaxError.new("unexpected low surrogate", token) if low_surrogate?(code_point)
|
67
|
+
|
68
|
+
return [code_point, index + 3] unless high_surrogate?(code_point)
|
69
|
+
|
70
|
+
unless index + 9 < length && value[index + 4] == "\\" && value[index + 5] == "u"
|
71
|
+
raise LiquidSyntaxError.new("incomplete escape sequence", token)
|
72
|
+
end
|
73
|
+
|
74
|
+
low_surrogate = parse_hex_digits(value[index + 6, 10] || raise, token)
|
75
|
+
|
76
|
+
unless low_surrogate?(low_surrogate)
|
77
|
+
raise LiquidSyntaxError.new("unexpected low surrogate",
|
78
|
+
token)
|
79
|
+
end
|
80
|
+
|
81
|
+
code_point = 0x10000 + (
|
82
|
+
((code_point & 0x03FF) << 10) | (low_surrogate & 0x03FF)
|
83
|
+
)
|
84
|
+
|
85
|
+
[code_point, index + 9]
|
86
|
+
end
|
87
|
+
|
88
|
+
def self.parse_hex_digits(digits, token)
|
89
|
+
code_point = 0
|
90
|
+
digits.each_byte do |b|
|
91
|
+
code_point <<= 4
|
92
|
+
case b
|
93
|
+
when 48..57
|
94
|
+
code_point |= b - 48
|
95
|
+
when 65..70
|
96
|
+
code_point |= b - 65 + 10
|
97
|
+
when 97..102
|
98
|
+
code_point |= b - 97 + 10
|
99
|
+
else
|
100
|
+
raise LiquidSyntaxError.new("invalid escape sequence", token)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
code_point
|
104
|
+
end
|
105
|
+
|
106
|
+
def self.high_surrogate?(code_point)
|
107
|
+
code_point.between?(0xD800, 0xDBFF)
|
108
|
+
end
|
109
|
+
|
110
|
+
def self.low_surrogate?(code_point)
|
111
|
+
code_point.between?(0xDC00, 0xDFFF)
|
112
|
+
end
|
113
|
+
|
114
|
+
def self.code_point_to_string(code_point, token)
|
115
|
+
raise LiquidSyntaxError.new("invalid character", token) if code_point <= 8
|
116
|
+
|
117
|
+
code_point.chr(Encoding::UTF_8)
|
118
|
+
end
|
119
|
+
end
|
data/lib/liquid2.rb
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
require_relative "liquid2/environment"
|
5
|
+
require_relative "liquid2/context"
|
6
|
+
require_relative "liquid2/filter"
|
7
|
+
require_relative "liquid2/scanner"
|
8
|
+
require_relative "liquid2/loader"
|
9
|
+
require_relative "liquid2/parser"
|
10
|
+
require_relative "liquid2/version"
|
11
|
+
require_relative "liquid2/undefined"
|
12
|
+
require_relative "liquid2/utils/chain_hash"
|
13
|
+
require_relative "liquid2/utils/unescape"
|
14
|
+
require_relative "liquid2/static_analysis"
|
15
|
+
require_relative "liquid2/loaders/file_system_loader"
|
16
|
+
|
17
|
+
# Liquid template engine.
|
18
|
+
module Liquid2
|
19
|
+
DEFAULT_ENVIRONMENT = Environment.new
|
20
|
+
|
21
|
+
# Parse _source_ text as a template using the default Liquid environment.
|
22
|
+
# @param source [String]
|
23
|
+
# @param data [?Hash[String, untyped]?]
|
24
|
+
# @return [Template]
|
25
|
+
def self.parse(source, globals: nil)
|
26
|
+
DEFAULT_ENVIRONMENT.parse(source, globals: globals)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Parse and render template _source_ with _data_ as template variables and
|
30
|
+
# the default Liquid environment.
|
31
|
+
# @param source [String]
|
32
|
+
# @param data [?Hash[String, untyped]?]
|
33
|
+
# @return [String]
|
34
|
+
def self.render(source, data = nil)
|
35
|
+
DEFAULT_ENVIRONMENT.render(source, data)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Stringify an object. Use this anywhere a string is expected, like in a filter.
|
39
|
+
def self.to_liquid_string(obj)
|
40
|
+
case obj
|
41
|
+
when Hash, Array
|
42
|
+
JSON.generate(obj)
|
43
|
+
else
|
44
|
+
obj.to_s
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Stringify an object for output. Use this when writing directly to an output buffer.
|
49
|
+
def self.to_output_string(obj)
|
50
|
+
case obj
|
51
|
+
when Array
|
52
|
+
# Concatenate string representations of array elements.
|
53
|
+
obj.map do |item|
|
54
|
+
Liquid2.to_s(item)
|
55
|
+
end.join
|
56
|
+
when BigDecimal
|
57
|
+
# TODO: test capture
|
58
|
+
# TODO: are there any scenarios where we need to cast to_f before output?
|
59
|
+
obj.to_f.to_s
|
60
|
+
else
|
61
|
+
Liquid2.to_s(obj)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def self.to_liquid_int(obj, default: 0)
|
66
|
+
Float(obj).to_i
|
67
|
+
rescue ArgumentError, TypeError
|
68
|
+
default
|
69
|
+
end
|
70
|
+
|
71
|
+
# Return `true` if _obj_ is Liquid truthy.
|
72
|
+
# @param context [RenderContext]
|
73
|
+
# @param obj [Object]
|
74
|
+
# @return [bool]
|
75
|
+
def self.truthy?(context, obj)
|
76
|
+
obj = obj.to_liquid(context) if obj.respond_to?(:to_liquid)
|
77
|
+
!!obj
|
78
|
+
end
|
79
|
+
|
80
|
+
# Return `true` if _obj_ is undefined.
|
81
|
+
def self.undefined?(obj)
|
82
|
+
obj.is_a?(Undefined)
|
83
|
+
end
|
84
|
+
|
85
|
+
class << self
|
86
|
+
alias to_s to_liquid_string
|
87
|
+
alias to_output_s to_output_string
|
88
|
+
alias to_i to_liquid_int
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "benchmark/ips"
|
4
|
+
require "json"
|
5
|
+
require "optparse"
|
6
|
+
require "pathname"
|
7
|
+
require "liquid2"
|
8
|
+
|
9
|
+
# A benchmark fixture
|
10
|
+
class Fixture
|
11
|
+
attr_reader :templates, :data, :name
|
12
|
+
|
13
|
+
# @param path [Pathname]
|
14
|
+
def initialize(path)
|
15
|
+
@root = path
|
16
|
+
@name = @root.basename.to_s
|
17
|
+
# rubocop:disable Style/StringConcatenation
|
18
|
+
@data = JSON.parse((@root + "data.json").read)
|
19
|
+
@templates = (@root + "templates").glob("*liquid").to_h { |p| [p.basename.to_s, p.read] }
|
20
|
+
# rubocop:enable Style/StringConcatenation
|
21
|
+
end
|
22
|
+
|
23
|
+
def env
|
24
|
+
Liquid2::Environment.new(loader: Liquid2::HashLoader.new(@templates), globals: @data)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
options = {
|
29
|
+
fixture: "002"
|
30
|
+
}
|
31
|
+
|
32
|
+
OptionParser.new do |parser|
|
33
|
+
parser.banner = <<~BANNER
|
34
|
+
Run one of the benchmarks in ./tests/cts/benchmark_fixtures.
|
35
|
+
Example: ruby benchmark.rb -f 002
|
36
|
+
BANNER
|
37
|
+
|
38
|
+
parser.on("-f FIXTURE", "--fixture FIXTURE",
|
39
|
+
"The name of the benchmark fixture to run. Defaults to '002'.") do |value|
|
40
|
+
options[:fixture] = value
|
41
|
+
end
|
42
|
+
|
43
|
+
parser.parse!
|
44
|
+
end
|
45
|
+
|
46
|
+
fixture = Fixture.new(Pathname.new("test/golden_liquid/benchmark_fixtures") + options[:fixture])
|
47
|
+
env = fixture.env
|
48
|
+
source = fixture.templates["index.liquid"]
|
49
|
+
template = env.get_template("index.liquid")
|
50
|
+
|
51
|
+
# scanner = StringScanner.new("")
|
52
|
+
|
53
|
+
Benchmark.ips do |x|
|
54
|
+
# Configure the number of seconds used during
|
55
|
+
# the warmup phase (default 2) and calculation phase (default 5)
|
56
|
+
x.config(warmup: 2, time: 5)
|
57
|
+
|
58
|
+
# x.report("tokenize (#{fixture.name}):") do
|
59
|
+
# Liquid2::Scanner.tokenize(source, scanner)
|
60
|
+
# end
|
61
|
+
|
62
|
+
x.report("parse (#{fixture.name}):") do
|
63
|
+
env.parse(source)
|
64
|
+
end
|
65
|
+
|
66
|
+
x.report("render (#{fixture.name}):") do
|
67
|
+
template.render
|
68
|
+
end
|
69
|
+
|
70
|
+
x.report("both (#{fixture.name}):") do
|
71
|
+
env.parse(source).render
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "memory_profiler"
|
4
|
+
require "json"
|
5
|
+
require "optparse"
|
6
|
+
require "pathname"
|
7
|
+
require "liquid2"
|
8
|
+
|
9
|
+
# A benchmark fixture
|
10
|
+
class Fixture
|
11
|
+
attr_reader :templates, :data
|
12
|
+
|
13
|
+
# @param path [Pathname]
|
14
|
+
def initialize(path)
|
15
|
+
@root = path
|
16
|
+
@name = @root.basename.to_s
|
17
|
+
# rubocop:disable Style/StringConcatenation
|
18
|
+
@data = JSON.parse((@root + "data.json").read)
|
19
|
+
@templates = (@root + "templates").glob("*liquid").to_h { |p| [p.basename.to_s, p.read] }
|
20
|
+
# rubocop:enable Style/StringConcatenation
|
21
|
+
end
|
22
|
+
|
23
|
+
def env
|
24
|
+
Liquid2::Environment.new(loader: Liquid2::HashLoader.new(@templates), globals: @data)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# TODO: add an option to profile parsing or rendering of Liquid templates
|
29
|
+
|
30
|
+
options = {
|
31
|
+
fixture: "002"
|
32
|
+
}
|
33
|
+
|
34
|
+
OptionParser.new do |parser|
|
35
|
+
parser.banner = <<~BANNER
|
36
|
+
Run one of the benchmarks in ./tests/cts/benchmark_fixtures.
|
37
|
+
Example: ruby benchmark.rb -f 002
|
38
|
+
BANNER
|
39
|
+
|
40
|
+
parser.on("-f FIXTURE", "--fixture FIXTURE",
|
41
|
+
"The name of the benchmark fixture to run. Defaults to '002'.") do |value|
|
42
|
+
options[:fixture] = value
|
43
|
+
end
|
44
|
+
|
45
|
+
parser.parse!
|
46
|
+
end
|
47
|
+
|
48
|
+
fixture = Fixture.new(Pathname.new("test/golden_liquid/benchmark_fixtures") + options[:fixture])
|
49
|
+
env = fixture.env
|
50
|
+
# source = fixture.templates["index.liquid"]
|
51
|
+
template = env.get_template("index.liquid")
|
52
|
+
|
53
|
+
n = 1000
|
54
|
+
|
55
|
+
report = MemoryProfiler.report do
|
56
|
+
n.times do
|
57
|
+
template.render
|
58
|
+
# Liquid2.tokenize(source)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
report.pretty_print
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "stackprof"
|
4
|
+
require "optparse"
|
5
|
+
require "pathname"
|
6
|
+
require "json"
|
7
|
+
require "liquid2"
|
8
|
+
|
9
|
+
# A benchmark fixture
|
10
|
+
class Fixture
|
11
|
+
attr_reader :templates, :data
|
12
|
+
|
13
|
+
# @param path [Pathname]
|
14
|
+
def initialize(path)
|
15
|
+
@root = path
|
16
|
+
@name = @root.basename.to_s
|
17
|
+
# rubocop:disable Style/StringConcatenation
|
18
|
+
@data = JSON.parse((@root + "data.json").read)
|
19
|
+
@templates = (@root + "templates").glob("*liquid").to_h { |p| [p.basename.to_s, p.read] }
|
20
|
+
# rubocop:enable Style/StringConcatenation
|
21
|
+
end
|
22
|
+
|
23
|
+
def env
|
24
|
+
Liquid2::Environment.new(loader: Liquid2::HashLoader.new(@templates), globals: @data)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
options = {
|
29
|
+
fixture: "002"
|
30
|
+
}
|
31
|
+
|
32
|
+
OptionParser.new do |parser|
|
33
|
+
parser.banner = <<~BANNER
|
34
|
+
Run one of the benchmarks in ./tests/cts/benchmark_fixtures.
|
35
|
+
Example: ruby benchmark.rb -f 002
|
36
|
+
BANNER
|
37
|
+
|
38
|
+
parser.on("-f FIXTURE", "--fixture FIXTURE",
|
39
|
+
"The name of the benchmark fixture to run. Defaults to '002'.") do |value|
|
40
|
+
options[:fixture] = value
|
41
|
+
end
|
42
|
+
|
43
|
+
parser.parse!
|
44
|
+
end
|
45
|
+
|
46
|
+
fixture = Fixture.new(Pathname.new("test/golden_liquid/benchmark_fixtures") + options[:fixture])
|
47
|
+
env = fixture.env
|
48
|
+
source = fixture.templates["index.liquid"]
|
49
|
+
template = env.get_template("index.liquid")
|
50
|
+
|
51
|
+
n = 1000
|
52
|
+
|
53
|
+
scanner = StringScanner.new("")
|
54
|
+
|
55
|
+
StackProf.run(mode: :cpu, raw: true, out: ".stackprof-cpu-scan.dump") do
|
56
|
+
n.times do
|
57
|
+
Liquid2::Scanner.tokenize(source, scanner)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
StackProf.run(mode: :cpu, raw: true, out: ".stackprof-cpu-parse.dump") do
|
62
|
+
n.times do
|
63
|
+
env.parse(source)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
StackProf.run(mode: :cpu, raw: true, out: ".stackprof-cpu-render.dump") do
|
68
|
+
n.times do
|
69
|
+
template.render
|
70
|
+
end
|
71
|
+
end
|