twig_ruby 0.0.1
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
- data/lib/twig/auto_hash.rb +17 -0
- data/lib/twig/cache/base.rb +31 -0
- data/lib/twig/cache/filesystem.rb +47 -0
- data/lib/twig/cache/nil.rb +19 -0
- data/lib/twig/callable.rb +21 -0
- data/lib/twig/compiler.rb +123 -0
- data/lib/twig/context.rb +64 -0
- data/lib/twig/environment.rb +161 -0
- data/lib/twig/error/base.rb +37 -0
- data/lib/twig/error/syntax.rb +8 -0
- data/lib/twig/expression_parser.rb +517 -0
- data/lib/twig/extension/base.rb +23 -0
- data/lib/twig/extension/core.rb +89 -0
- data/lib/twig/extension/rails.rb +70 -0
- data/lib/twig/extension_set.rb +69 -0
- data/lib/twig/lexer.rb +372 -0
- data/lib/twig/loader/array.rb +39 -0
- data/lib/twig/loader/base.rb +32 -0
- data/lib/twig/loader/filesystem.rb +45 -0
- data/lib/twig/node/base.rb +61 -0
- data/lib/twig/node/block.rb +20 -0
- data/lib/twig/node/block_reference.rb +17 -0
- data/lib/twig/node/empty.rb +11 -0
- data/lib/twig/node/expression/array.rb +50 -0
- data/lib/twig/node/expression/assign_name.rb +28 -0
- data/lib/twig/node/expression/base.rb +20 -0
- data/lib/twig/node/expression/binary/base.rb +63 -0
- data/lib/twig/node/expression/call.rb +28 -0
- data/lib/twig/node/expression/constant.rb +17 -0
- data/lib/twig/node/expression/filter.rb +52 -0
- data/lib/twig/node/expression/get_attribute.rb +30 -0
- data/lib/twig/node/expression/helper_method.rb +31 -0
- data/lib/twig/node/expression/name.rb +37 -0
- data/lib/twig/node/expression/ternary.rb +28 -0
- data/lib/twig/node/expression/unary/base.rb +52 -0
- data/lib/twig/node/expression/variable/assign_context.rb +11 -0
- data/lib/twig/node/expression/variable/context.rb +11 -0
- data/lib/twig/node/for.rb +64 -0
- data/lib/twig/node/for_loop.rb +39 -0
- data/lib/twig/node/if.rb +50 -0
- data/lib/twig/node/include.rb +71 -0
- data/lib/twig/node/module.rb +74 -0
- data/lib/twig/node/nodes.rb +13 -0
- data/lib/twig/node/print.rb +18 -0
- data/lib/twig/node/text.rb +20 -0
- data/lib/twig/node/yield.rb +54 -0
- data/lib/twig/output_buffer.rb +29 -0
- data/lib/twig/parser.rb +131 -0
- data/lib/twig/railtie.rb +60 -0
- data/lib/twig/source.rb +13 -0
- data/lib/twig/template.rb +50 -0
- data/lib/twig/token.rb +48 -0
- data/lib/twig/token_parser/base.rb +20 -0
- data/lib/twig/token_parser/block.rb +54 -0
- data/lib/twig/token_parser/extends.rb +25 -0
- data/lib/twig/token_parser/for.rb +64 -0
- data/lib/twig/token_parser/if.rb +64 -0
- data/lib/twig/token_parser/include.rb +51 -0
- data/lib/twig/token_parser/yield.rb +44 -0
- data/lib/twig/token_stream.rb +73 -0
- data/lib/twig/twig_filter.rb +21 -0
- data/lib/twig_ruby.rb +36 -0
- metadata +103 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 674b4b6d23d8a81c8dfe49d8b4c55766df7a2b110038f7dd04bb5dbed09095eb
|
4
|
+
data.tar.gz: 41320e049677ff964e222b3e65eb2aefe5d8513387834bed644d17abefd6caac
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: a9d8b98237d0c9e1955efd5bf1abf41877159bf590960c223dde2ca6a5a9c09ba6c1ce15d64c7d9beb0a29aa7eef3aa81d1166e45f572308faa249f67d106b73
|
7
|
+
data.tar.gz: c30fdc12854565c6c733eecf77ae9fce7e3134d67ca1630642ee21b1c5473a6e4fd2296ac82cf95abbe880a9de946a814562ce4913a73bbe5754bbccb571db89
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Twig
|
4
|
+
module Cache
|
5
|
+
class Base
|
6
|
+
# @param [String] name
|
7
|
+
# @param [String] class_name
|
8
|
+
def generate_key(name, class_name)
|
9
|
+
raise NotImplementedError
|
10
|
+
end
|
11
|
+
|
12
|
+
# @param [String] key
|
13
|
+
# @param [String] content
|
14
|
+
def write(key, content)
|
15
|
+
raise NotImplementedError
|
16
|
+
end
|
17
|
+
|
18
|
+
# @param [String] key
|
19
|
+
# @return [Boolean]
|
20
|
+
def load(key)
|
21
|
+
raise NotImplementedError
|
22
|
+
end
|
23
|
+
|
24
|
+
# @param [String] key
|
25
|
+
# @return [Integer]
|
26
|
+
def timestamp(key)
|
27
|
+
raise NotImplementedError
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Twig
|
4
|
+
module Cache
|
5
|
+
class Filesystem < Cache::Base
|
6
|
+
def initialize(directory)
|
7
|
+
super()
|
8
|
+
|
9
|
+
@directory = directory
|
10
|
+
end
|
11
|
+
|
12
|
+
def generate_key(_name, class_name)
|
13
|
+
hash = ::Digest::SHA256.hexdigest(class_name)
|
14
|
+
|
15
|
+
File.join(directory, hash[0] + hash[1], "#{hash}.rb")
|
16
|
+
end
|
17
|
+
|
18
|
+
def load(key)
|
19
|
+
if File.file?(key)
|
20
|
+
Kernel.load(key)
|
21
|
+
|
22
|
+
true
|
23
|
+
else
|
24
|
+
false
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def write(key, content)
|
29
|
+
dirname = File.dirname(key)
|
30
|
+
|
31
|
+
FileUtils.mkdir_p(dirname) unless File.directory?(dirname)
|
32
|
+
File.write(key, content)
|
33
|
+
end
|
34
|
+
|
35
|
+
def timestamp(key)
|
36
|
+
return 0 unless File.file?(key)
|
37
|
+
|
38
|
+
File.mtime(key).to_i
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
# @return [String]
|
44
|
+
attr_reader :directory
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Twig
|
4
|
+
module Cache
|
5
|
+
class Nil < Cache::Base
|
6
|
+
def generate_key(_name, _class_name)
|
7
|
+
''
|
8
|
+
end
|
9
|
+
|
10
|
+
def timestamp(_key)
|
11
|
+
0
|
12
|
+
end
|
13
|
+
|
14
|
+
def write(_key, _content); end
|
15
|
+
def load(_key); end
|
16
|
+
def remove(_key); end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Twig
|
4
|
+
class Callable
|
5
|
+
attr_reader :name, :dynamic_name, :callable
|
6
|
+
|
7
|
+
# @param [String] name
|
8
|
+
# @param [Proc|Nil] callable
|
9
|
+
# @param [Hash] options
|
10
|
+
def initialize(name, callable = nil, options = {})
|
11
|
+
@name = @dynamic_name = name
|
12
|
+
@callable = callable
|
13
|
+
@options = {
|
14
|
+
needs_environment: false,
|
15
|
+
needs_context: false,
|
16
|
+
needs_charset: false,
|
17
|
+
is_variadic: false,
|
18
|
+
}.merge(options)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Twig
|
4
|
+
# @!attribute [r] environment
|
5
|
+
# @return [Environment]
|
6
|
+
class Compiler
|
7
|
+
attr_reader :source, :environment
|
8
|
+
|
9
|
+
# @param [Environment] environment
|
10
|
+
def initialize(environment)
|
11
|
+
@environment = environment
|
12
|
+
@var_name_salt = 0
|
13
|
+
end
|
14
|
+
|
15
|
+
# @param [Node::Base] node
|
16
|
+
# @param [Integer] indentation
|
17
|
+
# @return [Compiler]
|
18
|
+
def compile(node, indentation = 0)
|
19
|
+
reset(indentation)
|
20
|
+
node.compile(self)
|
21
|
+
|
22
|
+
self
|
23
|
+
end
|
24
|
+
|
25
|
+
# @param [Node::Base] node
|
26
|
+
# @param [Boolean] raw
|
27
|
+
# @return [Compiler]
|
28
|
+
def subcompile(node, raw: true)
|
29
|
+
indent_source unless raw
|
30
|
+
|
31
|
+
node.compile(self)
|
32
|
+
|
33
|
+
self
|
34
|
+
end
|
35
|
+
|
36
|
+
# @param [Array<String>] strings
|
37
|
+
# @return [Compiler]
|
38
|
+
def write(*strings)
|
39
|
+
strings.each do |string|
|
40
|
+
indent_source
|
41
|
+
@source << string
|
42
|
+
end
|
43
|
+
|
44
|
+
self
|
45
|
+
end
|
46
|
+
|
47
|
+
# @param [String] value
|
48
|
+
# @return [Compiler]
|
49
|
+
def string(value)
|
50
|
+
@source << "%q[#{value}]"
|
51
|
+
|
52
|
+
self
|
53
|
+
end
|
54
|
+
|
55
|
+
# @param [String] string
|
56
|
+
# @return [Compiler]
|
57
|
+
def raw(string)
|
58
|
+
@source << string
|
59
|
+
|
60
|
+
self
|
61
|
+
end
|
62
|
+
|
63
|
+
def repr(value)
|
64
|
+
case value
|
65
|
+
when Integer, Float
|
66
|
+
raw(value.to_s)
|
67
|
+
when TrueClass, FalseClass
|
68
|
+
raw(value ? 'true' : 'false')
|
69
|
+
when NilClass
|
70
|
+
raw('nil')
|
71
|
+
when Array, Hash
|
72
|
+
raw('Marshal.load(').
|
73
|
+
raw(Marshal.dump(value).inspect).
|
74
|
+
raw(')')
|
75
|
+
when Symbol
|
76
|
+
raw(":#{value}")
|
77
|
+
else
|
78
|
+
string(value)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# @param [Integer] step
|
83
|
+
# @return [Compiler]
|
84
|
+
def indent(step = 1)
|
85
|
+
@indentation += step
|
86
|
+
|
87
|
+
self
|
88
|
+
end
|
89
|
+
|
90
|
+
# @param [Integer] step
|
91
|
+
# @return [Compiler]
|
92
|
+
def outdent(step = 1)
|
93
|
+
@indentation -= step
|
94
|
+
|
95
|
+
self
|
96
|
+
end
|
97
|
+
|
98
|
+
# @return [String]
|
99
|
+
def var_name
|
100
|
+
"_v#{@var_name_salt}"
|
101
|
+
end
|
102
|
+
|
103
|
+
private
|
104
|
+
|
105
|
+
def indent_source
|
106
|
+
@source << (' ' * @indentation)
|
107
|
+
end
|
108
|
+
|
109
|
+
# @return [Compiler]
|
110
|
+
def reset(indentation = 0)
|
111
|
+
@last_line = nil
|
112
|
+
@source = +''
|
113
|
+
@debug_info = []
|
114
|
+
@source_offset = 0
|
115
|
+
# source code starts at 1 (as we then increment it when we encounter new lines)
|
116
|
+
@source_line = 1
|
117
|
+
@indentation = indentation
|
118
|
+
@var_name_salt = 0
|
119
|
+
|
120
|
+
self
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
data/lib/twig/context.rb
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Twig
|
4
|
+
class Context < Hash
|
5
|
+
def initialize(initial_context = {})
|
6
|
+
super()
|
7
|
+
|
8
|
+
merge!(initial_context)
|
9
|
+
end
|
10
|
+
|
11
|
+
def merge!(other)
|
12
|
+
unless other.class <= Hash
|
13
|
+
raise 'Must merge! another Hash'
|
14
|
+
end
|
15
|
+
|
16
|
+
other.each do |k, v|
|
17
|
+
self[k] = v
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def merge(other)
|
22
|
+
self.class.new(self).merge!(other)
|
23
|
+
end
|
24
|
+
|
25
|
+
def push_stack
|
26
|
+
stack.push({ remove: [], replace: {} })
|
27
|
+
end
|
28
|
+
|
29
|
+
def pop_stack
|
30
|
+
return unless stack.last
|
31
|
+
|
32
|
+
frame = stack.pop
|
33
|
+
|
34
|
+
frame[:remove].each do |k|
|
35
|
+
delete(k)
|
36
|
+
end
|
37
|
+
frame[:replace].each { |k, v| self[k] = v }
|
38
|
+
end
|
39
|
+
|
40
|
+
def [](key)
|
41
|
+
super(key.to_sym)
|
42
|
+
end
|
43
|
+
|
44
|
+
def []=(key, value)
|
45
|
+
key = key.to_sym
|
46
|
+
|
47
|
+
if (frame = stack.last)
|
48
|
+
if key?(key) && !frame[:replace].key?(key) && !frame[:remove].include?(key)
|
49
|
+
frame[:replace][key] = self[key]
|
50
|
+
else
|
51
|
+
frame[:remove].push(key)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
super
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def stack
|
61
|
+
@stack ||= []
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,161 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Twig
|
4
|
+
class Environment
|
5
|
+
# @return [Cache::Base]
|
6
|
+
attr_reader :cache
|
7
|
+
|
8
|
+
# @param [::Twig::Loader::Base] loader
|
9
|
+
def initialize(loader, options = {})
|
10
|
+
@loader = loader
|
11
|
+
@extension_set = ExtensionSet.new
|
12
|
+
@options = {
|
13
|
+
cache: false,
|
14
|
+
debug: false,
|
15
|
+
auto_reload: nil,
|
16
|
+
}.merge(options)
|
17
|
+
|
18
|
+
@auto_reload = options[:auto_reload].nil? ? options[:debug] : options[:auto_reload]
|
19
|
+
|
20
|
+
self.cache = @options[:cache]
|
21
|
+
|
22
|
+
add_extension(Extension::Core.new)
|
23
|
+
end
|
24
|
+
|
25
|
+
def template_class(name)
|
26
|
+
key = loader.get_cache_key(name)
|
27
|
+
|
28
|
+
"Compiled::Template_#{::Digest::SHA256.hexdigest(key)}"
|
29
|
+
end
|
30
|
+
|
31
|
+
# @return [Twig::Template]
|
32
|
+
def load_template(name, **)
|
33
|
+
class_name = template_class(name)
|
34
|
+
cache_key = cache.generate_key(name, class_name)
|
35
|
+
|
36
|
+
attempt_cache = !@auto_reload || template_fresh?(name, cache.timestamp(cache_key))
|
37
|
+
|
38
|
+
if attempt_cache
|
39
|
+
@cache.load(cache_key)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Cache didn't load a class or we should load fresh
|
43
|
+
unless attempt_cache && Twig.const_defined?(class_name)
|
44
|
+
code = render_ruby(name)
|
45
|
+
|
46
|
+
# File cache loader won't rely on eval
|
47
|
+
@cache.write(cache_key, code)
|
48
|
+
@cache.load(cache_key)
|
49
|
+
|
50
|
+
# Finally just eval the generated code if cache does
|
51
|
+
# create the class
|
52
|
+
Twig.module_eval(code) unless Twig.const_defined?(class_name)
|
53
|
+
end
|
54
|
+
|
55
|
+
Twig.const_get(template_class(name)).
|
56
|
+
new(self, **)
|
57
|
+
end
|
58
|
+
|
59
|
+
def render(name)
|
60
|
+
loader.get_source_context(name).code
|
61
|
+
end
|
62
|
+
|
63
|
+
def render_ruby(name)
|
64
|
+
compile_source(
|
65
|
+
loader.get_source_context(name)
|
66
|
+
)
|
67
|
+
end
|
68
|
+
|
69
|
+
def extension(name)
|
70
|
+
@extension_set.extensions[name]
|
71
|
+
end
|
72
|
+
|
73
|
+
# @param [Extension::Base] extension
|
74
|
+
def add_extension(extension)
|
75
|
+
@extension_set.add(extension)
|
76
|
+
end
|
77
|
+
|
78
|
+
# @return [Array]
|
79
|
+
def operators
|
80
|
+
@extension_set.operators
|
81
|
+
end
|
82
|
+
|
83
|
+
# @return [TwigFilter]
|
84
|
+
def filter(name)
|
85
|
+
@extension_set.filter(name)
|
86
|
+
end
|
87
|
+
|
88
|
+
# @return [TokenParser::Base]
|
89
|
+
def token_parser(name)
|
90
|
+
@extension_set.token_parser(name)
|
91
|
+
end
|
92
|
+
|
93
|
+
# @return [Boolean]
|
94
|
+
def helper_method?(name)
|
95
|
+
@extension_set.helper_methods.include?(name)
|
96
|
+
end
|
97
|
+
|
98
|
+
# @param [Source] source
|
99
|
+
# @return [TokenStream]
|
100
|
+
def tokenize(source)
|
101
|
+
lexer.tokenize(source)
|
102
|
+
end
|
103
|
+
|
104
|
+
# @param [TokenStream] stream
|
105
|
+
def parse(stream)
|
106
|
+
parser.parse(stream)
|
107
|
+
end
|
108
|
+
|
109
|
+
# @param [Node::Base] node
|
110
|
+
def compile(node)
|
111
|
+
compiler.compile(node).source
|
112
|
+
end
|
113
|
+
|
114
|
+
# @param [Source] source
|
115
|
+
def compile_source(source)
|
116
|
+
compile(parse(tokenize(source)))
|
117
|
+
end
|
118
|
+
|
119
|
+
# @return [String]
|
120
|
+
def load_and_compile(name)
|
121
|
+
source = loader.get_source_context(name)
|
122
|
+
compile_source(source)
|
123
|
+
end
|
124
|
+
|
125
|
+
# @param [String] name
|
126
|
+
# @param [Integer] time
|
127
|
+
def template_fresh?(name, time)
|
128
|
+
# @todo check extension set last modified
|
129
|
+
loader.fresh?(name, time)
|
130
|
+
end
|
131
|
+
|
132
|
+
def cache=(cache)
|
133
|
+
@cache = if cache.is_a?(String)
|
134
|
+
Cache::Filesystem.new(cache)
|
135
|
+
elsif cache == false
|
136
|
+
Cache::Nil.new
|
137
|
+
elsif cache.class < Cache::Base
|
138
|
+
cache
|
139
|
+
else
|
140
|
+
raise "Cache must be string, false, or implement Twig::Cache::Base, got #{cache.inspect}"
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
private
|
145
|
+
|
146
|
+
# @return [Twig::Loader::Base]
|
147
|
+
attr_reader :loader
|
148
|
+
|
149
|
+
def lexer
|
150
|
+
@lexer ||= Lexer.new(self)
|
151
|
+
end
|
152
|
+
|
153
|
+
def parser
|
154
|
+
@parser ||= Parser.new(self)
|
155
|
+
end
|
156
|
+
|
157
|
+
def compiler
|
158
|
+
@compiler ||= Compiler.new(self)
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Twig
|
4
|
+
module Error
|
5
|
+
class Base < StandardError
|
6
|
+
attr_reader :lineno
|
7
|
+
|
8
|
+
# @param [String] message
|
9
|
+
# @param [Integer] lineno
|
10
|
+
# @param [Source] source
|
11
|
+
def initialize(message, lineno = -1, source = nil)
|
12
|
+
super(message)
|
13
|
+
|
14
|
+
if source
|
15
|
+
name = source.name
|
16
|
+
@source_code = source.code
|
17
|
+
@source_path = source.path
|
18
|
+
else
|
19
|
+
name = nil
|
20
|
+
end
|
21
|
+
|
22
|
+
@lineno = lineno
|
23
|
+
@name = name
|
24
|
+
@raw_message = message
|
25
|
+
end
|
26
|
+
|
27
|
+
def to_s
|
28
|
+
parts = [@raw_message]
|
29
|
+
parts << [" in #{@name}"] if @name
|
30
|
+
parts << [":#{@lineno}"] if @name && @lineno
|
31
|
+
parts << [" on line #{@lineno}"] if !@name && @lineno
|
32
|
+
|
33
|
+
parts.join
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|