orb_template 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.
Files changed (44) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +5 -0
  3. data/CODE_OF_CONDUCT.md +132 -0
  4. data/LICENSE.txt +21 -0
  5. data/Makefile +45 -0
  6. data/README.md +429 -0
  7. data/Rakefile +15 -0
  8. data/lib/orb/ast/abstract_node.rb +27 -0
  9. data/lib/orb/ast/attribute.rb +51 -0
  10. data/lib/orb/ast/block_node.rb +26 -0
  11. data/lib/orb/ast/control_expression_node.rb +27 -0
  12. data/lib/orb/ast/newline_node.rb +22 -0
  13. data/lib/orb/ast/printing_expression_node.rb +29 -0
  14. data/lib/orb/ast/private_comment_node.rb +22 -0
  15. data/lib/orb/ast/public_comment_node.rb +22 -0
  16. data/lib/orb/ast/root_node.rb +11 -0
  17. data/lib/orb/ast/tag_node.rb +208 -0
  18. data/lib/orb/ast/text_node.rb +22 -0
  19. data/lib/orb/ast.rb +19 -0
  20. data/lib/orb/document.rb +19 -0
  21. data/lib/orb/errors.rb +40 -0
  22. data/lib/orb/parser.rb +182 -0
  23. data/lib/orb/patterns.rb +40 -0
  24. data/lib/orb/rails_derp.rb +138 -0
  25. data/lib/orb/rails_template.rb +101 -0
  26. data/lib/orb/railtie.rb +9 -0
  27. data/lib/orb/render_context.rb +36 -0
  28. data/lib/orb/template.rb +72 -0
  29. data/lib/orb/temple/attributes_compiler.rb +114 -0
  30. data/lib/orb/temple/compiler.rb +204 -0
  31. data/lib/orb/temple/engine.rb +40 -0
  32. data/lib/orb/temple/filters.rb +132 -0
  33. data/lib/orb/temple/generators.rb +108 -0
  34. data/lib/orb/temple/identity.rb +16 -0
  35. data/lib/orb/temple/parser.rb +46 -0
  36. data/lib/orb/temple.rb +16 -0
  37. data/lib/orb/token.rb +47 -0
  38. data/lib/orb/tokenizer.rb +757 -0
  39. data/lib/orb/tokenizer2.rb +591 -0
  40. data/lib/orb/utils/erb.rb +40 -0
  41. data/lib/orb/utils/orb.rb +12 -0
  42. data/lib/orb/version.rb +5 -0
  43. data/lib/orb.rb +50 -0
  44. metadata +89 -0
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ORB
4
+ module Patterns
5
+ SPACE_CHARS = /\s/
6
+ TAG_NAME = %r{[^\s>/=$]+}
7
+ ATTRIBUTE_NAME = %r{[^\s>/=]+}
8
+ UNQUOTED_VALUE_INVALID_CHARS = /["'=<`]/
9
+ UNQUOTED_VALUE = %r{[^\s/>]+}
10
+ BLOCK_NAME_CHARS = /[^\s}]+/
11
+ START_TAG_START = /</
12
+ START_TAG_END = />/
13
+ START_TAG_END_SELF_CLOSING = %r{/>}
14
+ START_TAG_END_VERBATIM = /\$>/
15
+ END_TAG_START = %r{</}
16
+ END_TAG_END = />/
17
+ END_TAG_END_VERBATIM = /\$>/
18
+ PUBLIC_COMMENT_START = /<!--/
19
+ PUBLIC_COMMENT_END = /-->/
20
+ PRIVATE_COMMENT_START = /{!--/
21
+ PRIVATE_COMMENT_END = /--}/
22
+ PRINTING_EXPRESSION_START = /{{ */
23
+ PRINTING_EXPRESSION_END = / *}}/
24
+ CONTROL_EXPRESSION_START = /{% */
25
+ CONTROL_EXPRESSION_END = / *%}/
26
+ BLOCK_OPEN = /{#/
27
+ BLOCK_CLOSE = %r[{/]
28
+ ATTRIBUTE_ASSIGN = /=/
29
+ SINGLE_QUOTE = /'/
30
+ DOUBLE_QUOTE = /"/
31
+ SPLAT_START = /\*/
32
+ BRACE_OPEN = /\{/
33
+ BRACE_CLOSE = /\}/
34
+ CR = /\r/
35
+ NEWLINE = /\n/
36
+ CRLF = /\r\n/
37
+ BLANK = /[[:blank:]]/
38
+ OTHER = /./
39
+ end
40
+ end
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ # HACK DO NOT USE - THIS IF FOR DEBUGGING ERB ERROR SPOT HIGHLIGHTING ONLY
4
+ module ORB
5
+ class RailsDerp
6
+ class << self
7
+ def call(template, source = nil)
8
+ new.call(template, source)
9
+ end
10
+ end
11
+
12
+ # Specify trim mode for the ERB compiler. Defaults to '-'.
13
+ # See ERB documentation for suitable values.
14
+ class_attribute :erb_trim_mode, default: "-"
15
+
16
+ # Default implementation used.
17
+ # class_attribute :erb_implementation, default: Erubi
18
+
19
+ # Do not escape templates of these mime types.
20
+ class_attribute :escape_ignore_list, default: ["text/plain"]
21
+
22
+ # Strip trailing newlines from rendered output
23
+ class_attribute :strip_trailing_newlines, default: false
24
+
25
+ ENCODING_FLAG = '#.*coding[:=]\s*(\S+)[ \t]*'
26
+ ENCODING_TAG = Regexp.new("\\A(<%#{ENCODING_FLAG}-?%>)[ \\t]*")
27
+
28
+ # Translate an error location returned by ErrorHighlight to the correct
29
+ # source location inside the template.
30
+ def translate_location(spot, backtrace_location, source)
31
+ Rails.logger.debug "in translate_location"
32
+ Rails.logger.debug ""
33
+ Rails.logger.debug spot
34
+ Rails.logger.debug ""
35
+ Rails.logger.debug backtrace_location
36
+ Rails.logger.debug ""
37
+ Rails.logger.debug source
38
+
39
+ # Tokenize the source line
40
+ tokens = ORB::Utils::ERB.tokenize(source.lines[backtrace_location.lineno - 1])
41
+ new_first_column = find_offset(spot[:snippet], tokens, spot[:first_column])
42
+ lineno_delta = spot[:first_lineno] - backtrace_location.lineno
43
+ spot[:first_lineno] -= lineno_delta
44
+ spot[:last_lineno] -= lineno_delta
45
+
46
+ column_delta = spot[:first_column] - new_first_column
47
+ spot[:first_column] -= column_delta
48
+ spot[:last_column] -= column_delta
49
+ spot[:script_lines] = source.lines
50
+
51
+ spot
52
+ end
53
+
54
+ def call(template, source)
55
+ # First, convert to BINARY, so in case the encoding is
56
+ # wrong, we can still find an encoding tag
57
+ # (<%# encoding %>) inside the String using a regular
58
+ # expression
59
+ template_source = source.b
60
+
61
+ erb = template_source.gsub(ENCODING_TAG, "")
62
+ encoding = ::Regexp.last_match(2)
63
+
64
+ erb.force_encoding valid_encoding(source.dup, encoding)
65
+
66
+ # Always make sure we return a String in the default_internal
67
+ erb.encode!
68
+
69
+ # Strip trailing newlines from the template if enabled
70
+ erb.chomp! if strip_trailing_newlines
71
+
72
+ options = {
73
+ escape: (self.class.escape_ignore_list.include? template.type),
74
+ trim: (self.class.erb_trim_mode == "-")
75
+ }
76
+
77
+ if ActionView::Base.annotate_rendered_view_with_filenames && template.format == :html
78
+ options[:preamble] = "@output_buffer.safe_append='<!-- BEGIN #{template.short_identifier} -->';"
79
+ options[:postamble] = "@output_buffer.safe_append='<!-- END #{template.short_identifier} -->';@output_buffer"
80
+ end
81
+
82
+ ActionView::Template::Handlers::ERB::Erubi.new(erb, options).src
83
+ end
84
+
85
+ private
86
+
87
+ def valid_encoding(string, encoding)
88
+ # If a magic encoding comment was found, tag the
89
+ # String with this encoding. This is for a case
90
+ # where the original String was assumed to be,
91
+ # for instance, UTF-8, but a magic comment
92
+ # proved otherwise
93
+ string.force_encoding(encoding) if encoding
94
+
95
+ # If the String is valid, return the encoding we found
96
+ return string.encoding if string.valid_encoding?
97
+
98
+ # Otherwise, raise an exception
99
+ raise WrongEncodingError.new(string, string.encoding)
100
+ end
101
+
102
+ def find_offset(compiled, source_tokens, error_column)
103
+ compiled = StringScanner.new(compiled)
104
+
105
+ passed_tokens = []
106
+
107
+ while (tok = source_tokens.shift)
108
+ tok_name, str = *tok
109
+ case tok_name
110
+ when :TEXT
111
+ raise unless compiled.scan(str)
112
+ when :CODE
113
+ raise "We went too far" if compiled.pos > error_column
114
+
115
+ if compiled.pos + str.bytesize >= error_column
116
+ offset = error_column - compiled.pos
117
+ return passed_tokens.map(&:last).join.bytesize + offset
118
+ else
119
+ raise unless compiled.scan(str)
120
+ end
121
+ when :OPEN, :CLOSE
122
+ next_tok = source_tokens.first.last
123
+ loop do
124
+ break if compiled.match?(next_tok)
125
+
126
+ compiled.getch
127
+ end
128
+ else
129
+ raise NotImplemented, tok.first
130
+ end
131
+
132
+ passed_tokens << tok
133
+ end
134
+ end
135
+ end
136
+
137
+ ActionView::Template.register_template_handler(:derp, RailsDerp.new)
138
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ORB
4
+ class RailsTemplate
5
+ require 'orb/utils/orb'
6
+ # Compatible with: https://github.com/judofyr/temple/blob/v0.7.7/lib/temple/mixins/options.rb#L15-L24
7
+ class << self
8
+ def options
9
+ @options ||= {
10
+ generator: ::Temple::Generators::RailsOutputBuffer,
11
+ use_html_safe: true,
12
+ streaming: true,
13
+ buffer_class: 'ActionView::OutputBuffer',
14
+ disable_capture: true,
15
+ }
16
+ end
17
+
18
+ def set_options(opts)
19
+ options.update(opts)
20
+ end
21
+ end
22
+
23
+ def call(template, source = nil)
24
+ source ||= template.source
25
+ options = RailsTemplate.options
26
+
27
+ # Make the filename available in parser etc.
28
+ options = options.merge(file: template.identifier) if template.respond_to?(:identifier)
29
+
30
+ # Set type
31
+ options = options.merge(format: :xhtml) if template.respond_to?(:type) && template.type == 'text/xml'
32
+
33
+ # Annotations
34
+ if ActionView::Base.try(:annotate_rendered_view_with_filenames) && template.format == :html
35
+ options = options.merge(
36
+ preamble: "<!-- BEGIN #{template.short_identifier} -->\n",
37
+ postamble: "<!-- END #{template.short_identifier} -->\n",
38
+ )
39
+ end
40
+
41
+ # Pipe through the ORB Temple engine
42
+ ORB::Temple::Engine.new(options).call(source)
43
+ end
44
+
45
+ # See https://github.com/rails/rails/pull/47005
46
+ def translate_location(spot, backtrace_location, source)
47
+ offending_line_source = source.lines[backtrace_location.lineno - 1]
48
+ tokens = ORB::Utils::ORB.tokenize(offending_line_source)
49
+ new_column = find_offset(spot[:snippet], tokens, spot[:first_column])
50
+
51
+ lineno_delta = spot[:first_lineno] - backtrace_location.lineno
52
+ spot[:first_lineno] -= lineno_delta
53
+ spot[:last_lineno] -= lineno_delta
54
+
55
+ column_delta = spot[:first_column] - new_column
56
+ spot[:first_column] -= column_delta
57
+ spot[:last_column] -= column_delta
58
+ spot[:script_lines] = source.lines
59
+
60
+ spot
61
+ rescue StandardError => _e
62
+ spot
63
+ end
64
+
65
+ def find_offset(snippet, src_tokens, snippet_error_column)
66
+ offset = 0
67
+ passed_tokens = []
68
+
69
+ # Pass over tokens until we are just over the snippet error column
70
+ # then the column of the last token is the offset (without the token static offset for tags {% %})
71
+ while (tok = src_tokens.shift)
72
+ offset = snippet.index(tok.value, offset)
73
+ raise "text not found" unless offset
74
+ raise "we went too far" if offset > snippet_error_column
75
+
76
+ passed_tokens << tok
77
+ end
78
+ rescue StandardError
79
+ offset_token = passed_tokens.last
80
+ offset_from_token(offset_token)
81
+ end
82
+
83
+ def offset_from_token(token)
84
+ case token.type
85
+ when :tag_open, :tag_close
86
+ token.column + 1
87
+ when :public_comment, :private_comment
88
+ token.column + 4
89
+ when :block_open, :block_close
90
+ token.column + 2 + token.value.length
91
+ when :printing_expression, :control_expression
92
+ token.column + 2
93
+ end
94
+ end
95
+
96
+ def supports_streaming?
97
+ RailsTemplate.options[:streaming]
98
+ end
99
+ end
100
+ ActionView::Template.register_template_handler(:orb, RailsTemplate.new)
101
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ORB
4
+ class Railtie < ::Rails::Railtie
5
+ initializer :orb_template, before: :load_config_initializers do
6
+ require 'orb/rails_template'
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ORB
4
+ class RenderContext
5
+ def initialize(assigns = {})
6
+ @assigns = assigns
7
+ @errors = []
8
+ end
9
+
10
+ def []=(key, value)
11
+ @assigns[key] = value
12
+ end
13
+
14
+ def [](key)
15
+ resolve(key)
16
+ end
17
+
18
+ def has_key?(key)
19
+ resolve(key) != nil
20
+ end
21
+
22
+ attr_reader :errors
23
+
24
+ def binding
25
+ # rubocop:disable Style/OpenStructUse
26
+ OpenStruct.new(@assigns).instance_eval { binding }
27
+ # rubocop:enable Style/OpenStructUse
28
+ end
29
+
30
+ private
31
+
32
+ def resolve(key)
33
+ @assigns[key]
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ORB
4
+ class Template
5
+ attr_reader :doc
6
+
7
+ class << self
8
+ # create a new `Template` instance and parse the given source
9
+ def parse(source, opts = {})
10
+ template = Template.new(**opts)
11
+ template.parse(source)
12
+ template
13
+ end
14
+ end
15
+
16
+ # Create a new `Template` instance. Use `Template.parse` instead.
17
+ def initialize(**opts)
18
+ @options = opts
19
+ end
20
+
21
+ # Parses the given `source` and returns `self` for chaining.
22
+ def parse(source)
23
+ @doc = Document.new(tokenize(source))
24
+ self
25
+ end
26
+
27
+ # Local assigns
28
+ def assigns
29
+ @assigns ||= {}
30
+ end
31
+
32
+ # Parsing and rendering errors
33
+ def errors
34
+ @errors ||= []
35
+ end
36
+
37
+ # Render the template with the given `assigns`, which is a hash of local variables.
38
+ def render(*args)
39
+ # if we don't have a Document node, render to an empty string
40
+ retun "" unless @doc
41
+
42
+ # Determine the rendering context, which can either be a hash of local variables
43
+ # or an instance of `ORB::RenderContext`.
44
+ render_context = case args.first
45
+ when ORB::RenderContext
46
+ args.shift
47
+ when Hash
48
+ assigns.merge!(args.shift)
49
+ ORB::RenderContext.new(assigns)
50
+ when nil
51
+ ORB::RenderContext.new(assigns)
52
+ else
53
+ raise ArgumentError, "Expected a hash of local assigns or a ORB::RenderContext."
54
+ end
55
+
56
+ # Render loop
57
+ begin
58
+ @doc.render(render_context)
59
+ ensure
60
+ @errors = render_context.errors
61
+ end
62
+ end
63
+
64
+ private
65
+
66
+ def tokenize(source)
67
+ return [] if source.nil? || source.empty?
68
+
69
+ ORB::Tokenizer2.new(source, **@options).tokenize!
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ORB
4
+ module Temple
5
+ class AttributesCompiler
6
+ def initialize(options = {})
7
+ @options = options
8
+ end
9
+
10
+ # Compile the given array of AST::Attribute objects into Temple capture expressions
11
+ # using the given prefix in variable names
12
+ def compile_captures(attributes, prefix)
13
+ result = []
14
+
15
+ attributes.each do |attribute|
16
+ # TODO: handle splat attributes
17
+ next if attribute.splat?
18
+
19
+ # generate a unique variable name for the attribute
20
+ var_name = prefixed_variable_name(attribute.name, prefix)
21
+
22
+ # inject a code expression for the attribute value and assign to the variable
23
+ if attribute.string?
24
+ result << [:code, "#{var_name} = \"#{attribute.value}\""]
25
+ elsif attribute.bool?
26
+ result << [:code, "#{var_name} = true"]
27
+ elsif attribute.expression?
28
+ result << [:code, "#{var_name} = #{attribute.value}"]
29
+ end
30
+ end
31
+
32
+ result
33
+ end
34
+
35
+ # Compile the given array of AST::Attribute objects into a string of arguments
36
+ # the can be used in a ViewComponent constructor call, as long as the
37
+ # compiled captures are available in the same scope.
38
+ def compile_komponent_args(attributes, prefix)
39
+ args = {}
40
+ attributes.each do |attribute|
41
+ # TODO: handle splat attributes
42
+ next if attribute.splat?
43
+
44
+ var_name = prefixed_variable_name(attribute.name, prefix)
45
+ args = args.deep_merge(dash_to_hash(attribute.name, var_name))
46
+ end
47
+
48
+ hash_to_args_list(args)
49
+ end
50
+
51
+ # Compile the attributes of a node into a Temple core abstraction
52
+ def compile_attributes(attributes)
53
+ temple = [:html, :attrs]
54
+
55
+ attributes.each do |attribute|
56
+ # Ignore splat attributes
57
+ next if attribute.splat?
58
+
59
+ temple << compile_attribute(attribute)
60
+ end
61
+
62
+ temple
63
+ end
64
+
65
+ ##
66
+ # Compile splat attributes to a code string
67
+ def compile_splat_attributes(attributes)
68
+ attributes.map(&:value).join(',')
69
+ end
70
+
71
+ # Compile a single attribute into Temple core abstraction
72
+ # an attribute can be a static string, a dynamic expression,
73
+ # or a boolean attribute (an attribute without a value, e.g. disabled, checked, etc.)
74
+ #
75
+ # For boolean attributes, we return a [:dynamic, "nil"] expression, so that the
76
+ # final render for the attribute will be `attribute` instead of `attribute="true"`
77
+ def compile_attribute(attribute)
78
+ if attribute.string?
79
+ [:html, :attr, attribute.name, [:static, attribute.value]]
80
+ elsif attribute.bool?
81
+ [:html, :attr, attribute.name, [:dynamic, "nil"]]
82
+ elsif attribute.expression?
83
+ [:html, :attr, attribute.name, [:dynamic, attribute.value]]
84
+ end
85
+ end
86
+
87
+ def dash_to_hash(name, value)
88
+ parts = name.split('-')
89
+ parts.reverse.inject(value) { |a, n| { n => a } }
90
+ end
91
+
92
+ def hash_to_args_list(obj, level = -1)
93
+ case obj
94
+ when String
95
+ obj
96
+ when Array
97
+ obj.map { |v| hash_to_args_list(v, level + 1) }.join(", ")
98
+ when Hash
99
+ down_the_rabbit_hole = obj.map { |k, v| "#{k}: #{hash_to_args_list(v, level + 1)}" }.join(", ")
100
+ return down_the_rabbit_hole if level.negative?
101
+
102
+ "{#{down_the_rabbit_hole}}"
103
+
104
+ else
105
+ raise "Invalid argument passed to hash_to_args_list: #{obj.inspect}"
106
+ end
107
+ end
108
+
109
+ def prefixed_variable_name(name, prefix)
110
+ "#{prefix}_arg_#{name.underscore}"
111
+ end
112
+ end
113
+ end
114
+ end