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.
Files changed (64) hide show
  1. checksums.yaml +7 -0
  2. data/lib/twig/auto_hash.rb +17 -0
  3. data/lib/twig/cache/base.rb +31 -0
  4. data/lib/twig/cache/filesystem.rb +47 -0
  5. data/lib/twig/cache/nil.rb +19 -0
  6. data/lib/twig/callable.rb +21 -0
  7. data/lib/twig/compiler.rb +123 -0
  8. data/lib/twig/context.rb +64 -0
  9. data/lib/twig/environment.rb +161 -0
  10. data/lib/twig/error/base.rb +37 -0
  11. data/lib/twig/error/syntax.rb +8 -0
  12. data/lib/twig/expression_parser.rb +517 -0
  13. data/lib/twig/extension/base.rb +23 -0
  14. data/lib/twig/extension/core.rb +89 -0
  15. data/lib/twig/extension/rails.rb +70 -0
  16. data/lib/twig/extension_set.rb +69 -0
  17. data/lib/twig/lexer.rb +372 -0
  18. data/lib/twig/loader/array.rb +39 -0
  19. data/lib/twig/loader/base.rb +32 -0
  20. data/lib/twig/loader/filesystem.rb +45 -0
  21. data/lib/twig/node/base.rb +61 -0
  22. data/lib/twig/node/block.rb +20 -0
  23. data/lib/twig/node/block_reference.rb +17 -0
  24. data/lib/twig/node/empty.rb +11 -0
  25. data/lib/twig/node/expression/array.rb +50 -0
  26. data/lib/twig/node/expression/assign_name.rb +28 -0
  27. data/lib/twig/node/expression/base.rb +20 -0
  28. data/lib/twig/node/expression/binary/base.rb +63 -0
  29. data/lib/twig/node/expression/call.rb +28 -0
  30. data/lib/twig/node/expression/constant.rb +17 -0
  31. data/lib/twig/node/expression/filter.rb +52 -0
  32. data/lib/twig/node/expression/get_attribute.rb +30 -0
  33. data/lib/twig/node/expression/helper_method.rb +31 -0
  34. data/lib/twig/node/expression/name.rb +37 -0
  35. data/lib/twig/node/expression/ternary.rb +28 -0
  36. data/lib/twig/node/expression/unary/base.rb +52 -0
  37. data/lib/twig/node/expression/variable/assign_context.rb +11 -0
  38. data/lib/twig/node/expression/variable/context.rb +11 -0
  39. data/lib/twig/node/for.rb +64 -0
  40. data/lib/twig/node/for_loop.rb +39 -0
  41. data/lib/twig/node/if.rb +50 -0
  42. data/lib/twig/node/include.rb +71 -0
  43. data/lib/twig/node/module.rb +74 -0
  44. data/lib/twig/node/nodes.rb +13 -0
  45. data/lib/twig/node/print.rb +18 -0
  46. data/lib/twig/node/text.rb +20 -0
  47. data/lib/twig/node/yield.rb +54 -0
  48. data/lib/twig/output_buffer.rb +29 -0
  49. data/lib/twig/parser.rb +131 -0
  50. data/lib/twig/railtie.rb +60 -0
  51. data/lib/twig/source.rb +13 -0
  52. data/lib/twig/template.rb +50 -0
  53. data/lib/twig/token.rb +48 -0
  54. data/lib/twig/token_parser/base.rb +20 -0
  55. data/lib/twig/token_parser/block.rb +54 -0
  56. data/lib/twig/token_parser/extends.rb +25 -0
  57. data/lib/twig/token_parser/for.rb +64 -0
  58. data/lib/twig/token_parser/if.rb +64 -0
  59. data/lib/twig/token_parser/include.rb +51 -0
  60. data/lib/twig/token_parser/yield.rb +44 -0
  61. data/lib/twig/token_stream.rb +73 -0
  62. data/lib/twig/twig_filter.rb +21 -0
  63. data/lib/twig_ruby.rb +36 -0
  64. 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,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Twig
4
+ class AutoHash < Hash
5
+ def add(*values)
6
+ values.each do |value|
7
+ self[length] = value
8
+ end
9
+
10
+ self
11
+ end
12
+
13
+ def <<(*values)
14
+ add(*values)
15
+ end
16
+ end
17
+ end
@@ -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
@@ -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
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Twig
4
+ module Error
5
+ class Syntax < Error::Base
6
+ end
7
+ end
8
+ end