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
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Twig
4
+ module Node
5
+ class Module < Node::Base
6
+ def initialize(body, parent, blocks, source)
7
+ nodes = {
8
+ body:,
9
+ blocks:,
10
+ }
11
+ nodes[:parent] = parent if parent
12
+
13
+ super(nodes)
14
+
15
+ self.source_context = source
16
+ end
17
+
18
+ def compile(compiler)
19
+ class_begin = <<~CLASS
20
+ class Twig::#{compiler.environment.template_class(source_context.name)} < ::Twig::Template
21
+ CLASS
22
+
23
+ class_end = <<~CLASS
24
+ end
25
+ CLASS
26
+
27
+ compiler.
28
+ raw(class_begin).
29
+ indent.
30
+ write("def call(context = {}, blocks = {})\n").
31
+ indent
32
+
33
+ if nodes.key?(:parent)
34
+ compiler.
35
+ write('load_template(').
36
+ subcompile(nodes[:parent]).
37
+ raw(").render(context, block_list.merge(blocks));\n")
38
+ else
39
+ compiler.
40
+ subcompile(nodes[:body])
41
+ end
42
+
43
+ compiler.
44
+ write("@output_buffer\n").
45
+ outdent.
46
+ write("end\n\n").
47
+ subcompile(nodes[:blocks]).
48
+ outdent
49
+
50
+ compiler.
51
+ indent.
52
+ write("def block_list\n").
53
+ indent.
54
+ write("{\n").
55
+ indent
56
+
57
+ nodes[:blocks].nodes.each_value do |block|
58
+ compiler.
59
+ write("#{block.attributes[:name]}: self,\n")
60
+ end
61
+
62
+ compiler.
63
+ outdent.
64
+ write("}\n").
65
+ outdent.
66
+ write("end\n").
67
+ outdent
68
+
69
+ compiler.
70
+ raw(class_end)
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Twig
4
+ module Node
5
+ class Nodes < Node::Base
6
+ # @param [Hash<Node::Base>] nodes
7
+ # @param [Integer] lineno
8
+ def initialize(nodes, lineno = 0)
9
+ super(nodes, {}, lineno)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Twig
4
+ module Node
5
+ class Print < Node::Base
6
+ def initialize(expr, lineno)
7
+ super({ expr: }, {}, lineno)
8
+ end
9
+
10
+ def compile(compiler)
11
+ compiler.
12
+ write('@output_buffer.append = ').
13
+ subcompile(nodes[:expr]).
14
+ raw(";\n")
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Twig
4
+ module Node
5
+ class Text < Node::Base
6
+ # @param [String] data
7
+ # @param [Integer] lineno
8
+ def initialize(data, lineno = 0)
9
+ super({}, { data: }, lineno)
10
+ end
11
+
12
+ def compile(compiler)
13
+ compiler.
14
+ write('@output_buffer.safe_append = ').
15
+ string(attributes[:data]).
16
+ raw(";\n")
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Twig
4
+ module Node
5
+ class Yield < Node::Base
6
+ def initialize(expr, body, arguments, lineno)
7
+ arguments = arguments.empty? ? {} : { arguments: }
8
+ super({ expr:, body: }, arguments, lineno)
9
+ end
10
+
11
+ def compile(compiler)
12
+ compiler.
13
+ write('@output_buffer.append = (').
14
+ subcompile(nodes[:expr]).
15
+ raw(' do')
16
+
17
+ if attributes.key?(:arguments)
18
+ compiler.
19
+ raw(" |#{attributes[:arguments].join(', ')}|")
20
+ end
21
+
22
+ compiler.
23
+ raw("\n").
24
+ indent
25
+
26
+ if attributes.key?(:arguments)
27
+ compiler.
28
+ write("context.push_stack\n").
29
+ write('context.merge!({')
30
+
31
+ attributes[:arguments].each do |argument|
32
+ compiler.raw("#{argument}:,")
33
+ end
34
+
35
+ compiler.
36
+ raw("})\n")
37
+ end
38
+
39
+ compiler.
40
+ subcompile(nodes[:body]).
41
+ raw("\n")
42
+
43
+ if attributes.key?(:arguments)
44
+ compiler.
45
+ write("context.pop_stack\n")
46
+ end
47
+
48
+ compiler.
49
+ outdent.
50
+ write("end);\n")
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Twig
4
+ class OutputBuffer
5
+ def initialize
6
+ @buffer = +''
7
+ end
8
+
9
+ def append=(string)
10
+ unless string.nil?
11
+ string = string.to_s
12
+
13
+ @buffer << if string.html_safe?
14
+ string
15
+ else
16
+ CGI.escapeHTML(string)
17
+ end
18
+ end
19
+ end
20
+
21
+ def safe_append=(string)
22
+ @buffer << string.html_safe
23
+ end
24
+
25
+ def to_s
26
+ @buffer
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Twig
4
+ # @!attribute [r] stream
5
+ # @return [TokenStream]
6
+ class Parser
7
+ attr_reader :stream, :block_stack
8
+
9
+ # @param [Environment] environment
10
+ def initialize(environment)
11
+ @environment = environment
12
+ end
13
+
14
+ # @param [TokenStream] stream
15
+ def parse(stream, test = nil, drop_needle: false)
16
+ @stream = stream
17
+ @parent = nil
18
+ @blocks = {}
19
+ @block_stack = []
20
+ @imported_symbols = [{}]
21
+
22
+ body = subparse(test, drop_needle:)
23
+
24
+ Node::Module.new(body, @parent, Node::Nodes.new(@blocks), stream.source)
25
+ end
26
+
27
+ # @param [Proc] test
28
+ # @return [Node::Base]
29
+ def subparse(test, drop_needle: false)
30
+ lineno = current_token.lineno
31
+ rv = AutoHash.new
32
+
33
+ until stream.eof?
34
+ case current_token.type
35
+ when Token::TEXT_TYPE
36
+ token = stream.next
37
+ rv.add(Node::Text.new(token.value, token.lineno))
38
+ when Token::VAR_START_TYPE
39
+ token = stream.next
40
+ expr = expression_parser.parse_expression
41
+ stream.expect(Token::VAR_END_TYPE)
42
+
43
+ rv.add(Node::Print.new(expr, token.lineno))
44
+ when Token::BLOCK_START_TYPE
45
+ stream.next
46
+ token = current_token
47
+
48
+ unless token.type == Token::NAME_TYPE
49
+ raise Error::Syntax.new('A block must start with a tag name.', token.lineno, stream.source)
50
+ end
51
+
52
+ if test&.call(token)
53
+ stream.next if drop_needle
54
+ return rv.values.first if rv.length == 1
55
+
56
+ return Node::Nodes.new(rv, lineno)
57
+ end
58
+
59
+ # @todo Check that there is a token parser for this token value
60
+ subparser = @environment.token_parser(token.value)
61
+
62
+ unless subparser
63
+ raise Error::Syntax.new("Unexpected '#{token.value}' tag.", token.lineno, stream.source)
64
+ end
65
+
66
+ stream.next
67
+ subparser.parser = self
68
+ node = subparser.parse(token)
69
+
70
+ raise 'Cannot return nil from TokenParser' unless node
71
+
72
+ node.tag = subparser.tag
73
+
74
+ rv.add(node)
75
+ else
76
+ raise "Unable to parse token of type #{current_token.type}"
77
+ end
78
+ end
79
+
80
+ Node::Nodes.new(rv)
81
+ end
82
+
83
+ # @return [Token]
84
+ def current_token
85
+ stream.current
86
+ end
87
+
88
+ # @return [ExpressionParser]
89
+ def expression_parser
90
+ @expression_parser ||= ExpressionParser.new(self, @environment)
91
+ end
92
+
93
+ def push_local_scope
94
+ @imported_symbols.unshift({})
95
+ end
96
+
97
+ def pop_local_scope
98
+ @imported_symbols.shift
99
+ end
100
+
101
+ def peek_block_stack
102
+ @block_stack[-1]
103
+ end
104
+
105
+ def pop_block_stack
106
+ @block_stack.pop
107
+ end
108
+
109
+ def push_block_stack(name)
110
+ @block_stack << name
111
+ end
112
+
113
+ def block?(name)
114
+ @blocks.key?(name)
115
+ end
116
+
117
+ # @todo type value as BlockNode and also set it to a BodyNode
118
+ def set_block(name, value)
119
+ @blocks[name] = value
120
+ end
121
+
122
+ # @param [Node::Base] parent
123
+ def parent=(parent)
124
+ if @parent
125
+ raise Error::Syntax.new('Cannot extends twice', parent.lineno, parent.source_context)
126
+ end
127
+
128
+ @parent = parent
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Twig
4
+ class RailsRenderer
5
+ def call(template, source)
6
+ <<~TEMPLATE
7
+ ::#{self.class.name}.
8
+ environment.
9
+ load_template("#{template.short_identifier}", call_context: self, output_buffer: @output_buffer).
10
+ render(local_assigns)
11
+
12
+ @output_buffer
13
+ TEMPLATE
14
+ end
15
+
16
+ def translate_location(spot, backtrace_location, source)
17
+ template = backtrace_location.path.delete_prefix(Rails.root.to_s)
18
+
19
+ # Attempt to recompile the template to find where the syntax error is
20
+ # otherwise just do what would have happened anyway
21
+ begin
22
+ self.class.environment.render_ruby(template)
23
+ rescue ::Twig::Error::Syntax => e
24
+ return spot.merge({
25
+ first_lineno: e.lineno,
26
+ last_lineno: e.lineno + 1,
27
+ script_lines: source.lines,
28
+ })
29
+ rescue StandardError
30
+ # Nothing, don't add another exception to the problem
31
+ end
32
+
33
+ spot
34
+ end
35
+
36
+ def self.loader
37
+ @loader ||= ::Twig::Loader::Filesystem.new(
38
+ Rails.root,
39
+ %w[/ /app/views]
40
+ )
41
+ end
42
+
43
+ def self.environment
44
+ options = {
45
+ cache: Rails.root.join('tmp/cache/twig').to_s,
46
+ debug: Rails.env.development?,
47
+ }
48
+
49
+ @environment ||= ::Twig::Environment.new(loader, options).tap do |env|
50
+ env.add_extension(::Twig::Extension::Rails.new)
51
+ end
52
+ end
53
+ end
54
+
55
+ class Railtie < ::Rails::Railtie
56
+ initializer 'twig_ruby.configure_rails_initialization' do
57
+ ActionView::Template.register_template_handler :twig, RailsRenderer.new
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Twig
4
+ class Source
5
+ attr_reader :code, :name, :path
6
+
7
+ def initialize(code, name, path = '')
8
+ @code = code
9
+ @name = name
10
+ @path = path
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Twig
4
+ # Base class for compiled templates
5
+ class Template
6
+ ARRAY_CALL = :array_call
7
+ METHOD_CALL = :method_call
8
+ ANY_CALL = :any_call
9
+
10
+ # @param [Environment] environment
11
+ def initialize(environment, call_context: nil, output_buffer: nil)
12
+ @environment = environment
13
+ @parents = {}
14
+ @blocks = {}
15
+ @call_context = call_context
16
+ @output_buffer = output_buffer || OutputBuffer.new
17
+ end
18
+
19
+ def call(context = {}, blocks = {})
20
+ raise 'call is not implemented'
21
+ end
22
+
23
+ def render(context = {}, blocks = {})
24
+ call(Context.new(context), blocks)
25
+ end
26
+
27
+ def yield_block(name, context = {}, blocks = {})
28
+ object = self
29
+
30
+ if blocks.key?(name)
31
+ object = blocks[name]
32
+ end
33
+
34
+ object.public_send(:"block_#{name}", context, blocks)
35
+ end
36
+
37
+ private
38
+
39
+ # @param [String] name
40
+ # @return ]Template]
41
+ def load_template(name, template_name = '', template_line = nil)
42
+ env.load_template(name, call_context: @call_context, output_buffer: @output_buffer)
43
+ end
44
+
45
+ # @return [Environment]
46
+ def env
47
+ @environment
48
+ end
49
+ end
50
+ end
data/lib/twig/token.rb ADDED
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Twig
4
+ class Token
5
+ EOF_TYPE = :eof
6
+ TEXT_TYPE = :text
7
+ BLOCK_START_TYPE = :block_start
8
+ VAR_START_TYPE = :var_start
9
+ BLOCK_END_TYPE = :block_end
10
+ VAR_END_TYPE = :var_end
11
+ NAME_TYPE = :name
12
+ SYMBOL_TYPE = :symbol
13
+ NUMBER_TYPE = :number
14
+ STRING_TYPE = :string
15
+ OPERATOR_TYPE = :operator
16
+ PUNCTUATION_TYPE = :punctuation
17
+ INTERPOLATION_START_TYPE = :interpolation_start
18
+ INTERPOLATION_END_TYPE = :interpolation_end
19
+ ARROW_TYPE = :arrow
20
+ SPREAD_TYPE = :spread
21
+
22
+ attr_reader :type, :value, :lineno
23
+
24
+ def initialize(type, value, lineno)
25
+ @type = type
26
+ @value = value
27
+ @lineno = lineno
28
+ end
29
+
30
+ # @param [Symbol | String] type
31
+ def test(type, values = nil)
32
+ if values.nil? && !type.is_a?(Symbol)
33
+ values = type
34
+ type = NAME_TYPE
35
+ end
36
+
37
+ @type == type && (
38
+ values.nil? ||
39
+ (values.is_a?(Array) && values.include?(@value)) ||
40
+ (@value == values)
41
+ )
42
+ end
43
+
44
+ def debug
45
+ [type, value]
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Twig
4
+ module TokenParser
5
+ class Base
6
+ # @return [Parser]
7
+ attr_accessor :parser
8
+
9
+ # @param [Token] token
10
+ def parse(token)
11
+ raise 'parse is not implemented'
12
+ end
13
+
14
+ # @return [String]
15
+ def tag
16
+ raise 'tag is not implemented'
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Twig
4
+ module TokenParser
5
+ # Marks a section of a template as being reusable.
6
+ #
7
+ # {% block head %}
8
+ # <link rel="stylesheet" href="style.css" />
9
+ # <title>{% block title %}{% endblock %} - My Webpage</title>
10
+ # {% endblock %}
11
+ class Block < Base
12
+ def parse(token)
13
+ lineno = token.lineno
14
+ stream = parser.stream
15
+ name = stream.expect(Token::NAME_TYPE).value
16
+ block = Node::Block.new(name, Node::Empty.new, lineno)
17
+
18
+ parser.set_block(name, block)
19
+ parser.push_local_scope
20
+ parser.push_block_stack(name)
21
+
22
+ if stream.next_if(Token::BLOCK_END_TYPE)
23
+ body = parser.subparse(decide_block_end, drop_needle: true)
24
+
25
+ if (token = stream.next_if(Token::NAME_TYPE)) && token.value != name
26
+ raise "Expected end block for #{name}, given #{token.value}"
27
+ end
28
+ else
29
+ body = Node::Nodes.new({
30
+ 0 => Node::Print.new(parser.expression_parser.parse_expression, lineno),
31
+ })
32
+ end
33
+
34
+ stream.expect(Token::BLOCK_END_TYPE)
35
+ block.nodes[:body] = body
36
+
37
+ parser.pop_block_stack
38
+ parser.pop_local_scope
39
+
40
+ Node::BlockReference.new(name, lineno)
41
+ end
42
+
43
+ def tag
44
+ 'block'
45
+ end
46
+
47
+ private
48
+
49
+ def decide_block_end
50
+ ->(token) { token.test('endblock') }
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Twig
4
+ module TokenParser
5
+ class Extends < TokenParser::Base
6
+ def parse(token)
7
+ stream = parser.stream
8
+
9
+ if parser.peek_block_stack
10
+ raise Error::Syntax.new('Cannot raise from inside a block', token.lineno, stream.source)
11
+ # elsif parser.main_scope? @todo
12
+ end
13
+
14
+ parser.parent = parser.expression_parser.parse_expression
15
+ stream.expect(Token::BLOCK_END_TYPE)
16
+
17
+ Node::Empty.new(token.lineno)
18
+ end
19
+
20
+ def tag
21
+ 'extends'
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Twig
4
+ module TokenParser
5
+ # {% for user in users %}
6
+ # <li>{{ user.name }}</li>
7
+ # {% endfor %}
8
+ class For < Base
9
+ def parse(token)
10
+ lineno = token.lineno
11
+ stream = parser.stream
12
+
13
+ targets = parser.expression_parser.parse_assignment_expression
14
+ stream.expect(Token::OPERATOR_TYPE, 'in')
15
+ seq = parser.expression_parser.parse_expression
16
+
17
+ stream.expect(Token::BLOCK_END_TYPE)
18
+ body = parser.subparse(decide_for_fork)
19
+
20
+ if stream.next.value == 'else'
21
+ stream.expect(Token::BLOCK_END_TYPE)
22
+ else_expr = parser.subparse(decide_for_end, drop_needle: true)
23
+ else
24
+ else_expr = nil
25
+ end
26
+
27
+ stream.expect(Token::BLOCK_END_TYPE)
28
+
29
+ if targets.nodes.length > 1
30
+ key_target = targets.nodes[0]
31
+ key_target = Node::Expression::Variable::AssignContext.new(
32
+ key_target.attributes[:name],
33
+ key_target.lineno
34
+ )
35
+ value_target = targets.nodes[1]
36
+ else
37
+ key_target = Node::Expression::Variable::AssignContext.new('_key', lineno)
38
+ value_target = targets.nodes[0]
39
+ end
40
+
41
+ value_target = Node::Expression::Variable::AssignContext.new(
42
+ value_target.attributes[:name],
43
+ value_target.lineno
44
+ )
45
+
46
+ Node::For.new(key_target, value_target, seq, nil, body, else_expr, lineno)
47
+ end
48
+
49
+ def tag
50
+ 'for'
51
+ end
52
+
53
+ private
54
+
55
+ def decide_for_fork
56
+ ->(token) { token.test(%w[else endfor]) }
57
+ end
58
+
59
+ def decide_for_end
60
+ ->(token) { token.test(%w[endfor]) }
61
+ end
62
+ end
63
+ end
64
+ end