curlybars 0.9.13

Sign up to get free protection for your applications and to get access to all the features.
Files changed (77) hide show
  1. checksums.yaml +7 -0
  2. data/lib/curlybars.rb +108 -0
  3. data/lib/curlybars/configuration.rb +41 -0
  4. data/lib/curlybars/dependency_tracker.rb +8 -0
  5. data/lib/curlybars/error/base.rb +18 -0
  6. data/lib/curlybars/error/compile.rb +11 -0
  7. data/lib/curlybars/error/lex.rb +22 -0
  8. data/lib/curlybars/error/parse.rb +41 -0
  9. data/lib/curlybars/error/presenter/not_found.rb +23 -0
  10. data/lib/curlybars/error/render.rb +11 -0
  11. data/lib/curlybars/error/validate.rb +18 -0
  12. data/lib/curlybars/lexer.rb +60 -0
  13. data/lib/curlybars/method_whitelist.rb +69 -0
  14. data/lib/curlybars/node/block_helper_else.rb +108 -0
  15. data/lib/curlybars/node/boolean.rb +24 -0
  16. data/lib/curlybars/node/each_else.rb +69 -0
  17. data/lib/curlybars/node/if_else.rb +33 -0
  18. data/lib/curlybars/node/item.rb +31 -0
  19. data/lib/curlybars/node/literal.rb +28 -0
  20. data/lib/curlybars/node/option.rb +25 -0
  21. data/lib/curlybars/node/output.rb +24 -0
  22. data/lib/curlybars/node/partial.rb +24 -0
  23. data/lib/curlybars/node/path.rb +137 -0
  24. data/lib/curlybars/node/root.rb +29 -0
  25. data/lib/curlybars/node/string.rb +24 -0
  26. data/lib/curlybars/node/template.rb +32 -0
  27. data/lib/curlybars/node/text.rb +24 -0
  28. data/lib/curlybars/node/unless_else.rb +33 -0
  29. data/lib/curlybars/node/variable.rb +34 -0
  30. data/lib/curlybars/node/with_else.rb +54 -0
  31. data/lib/curlybars/parser.rb +183 -0
  32. data/lib/curlybars/position.rb +7 -0
  33. data/lib/curlybars/presenter.rb +288 -0
  34. data/lib/curlybars/processor/tilde.rb +31 -0
  35. data/lib/curlybars/processor/token_factory.rb +9 -0
  36. data/lib/curlybars/railtie.rb +18 -0
  37. data/lib/curlybars/rendering_support.rb +222 -0
  38. data/lib/curlybars/safe_buffer.rb +11 -0
  39. data/lib/curlybars/template_handler.rb +93 -0
  40. data/lib/curlybars/version.rb +3 -0
  41. data/spec/acceptance/application_layout_spec.rb +60 -0
  42. data/spec/acceptance/collection_blocks_spec.rb +28 -0
  43. data/spec/acceptance/global_helper_spec.rb +25 -0
  44. data/spec/curlybars/configuration_spec.rb +57 -0
  45. data/spec/curlybars/error/base_spec.rb +41 -0
  46. data/spec/curlybars/error/compile_spec.rb +19 -0
  47. data/spec/curlybars/error/lex_spec.rb +25 -0
  48. data/spec/curlybars/error/parse_spec.rb +74 -0
  49. data/spec/curlybars/error/render_spec.rb +19 -0
  50. data/spec/curlybars/error/validate_spec.rb +19 -0
  51. data/spec/curlybars/lexer_spec.rb +466 -0
  52. data/spec/curlybars/method_whitelist_spec.rb +168 -0
  53. data/spec/curlybars/processor/tilde_spec.rb +60 -0
  54. data/spec/curlybars/rendering_support_spec.rb +426 -0
  55. data/spec/curlybars/safe_buffer_spec.rb +46 -0
  56. data/spec/curlybars/template_handler_spec.rb +222 -0
  57. data/spec/integration/cache_spec.rb +124 -0
  58. data/spec/integration/comment_spec.rb +60 -0
  59. data/spec/integration/exception_spec.rb +31 -0
  60. data/spec/integration/node/block_helper_else_spec.rb +422 -0
  61. data/spec/integration/node/each_else_spec.rb +204 -0
  62. data/spec/integration/node/each_spec.rb +291 -0
  63. data/spec/integration/node/escape_spec.rb +27 -0
  64. data/spec/integration/node/helper_spec.rb +176 -0
  65. data/spec/integration/node/if_else_spec.rb +129 -0
  66. data/spec/integration/node/if_spec.rb +143 -0
  67. data/spec/integration/node/output_spec.rb +68 -0
  68. data/spec/integration/node/partial_spec.rb +66 -0
  69. data/spec/integration/node/path_spec.rb +286 -0
  70. data/spec/integration/node/root_spec.rb +15 -0
  71. data/spec/integration/node/template_spec.rb +86 -0
  72. data/spec/integration/node/unless_else_spec.rb +129 -0
  73. data/spec/integration/node/unless_spec.rb +130 -0
  74. data/spec/integration/node/with_spec.rb +116 -0
  75. data/spec/integration/processor/tilde_spec.rb +38 -0
  76. data/spec/integration/processors_spec.rb +30 -0
  77. metadata +358 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 301f06415961a13ec43df1d5e9a665a8acf4b82a1bf7c4b3b13c2c9a8e4927c1
4
+ data.tar.gz: 239584a824c472cfb8990eb5f5222c97af8099dcde44ae260570d9ca7cf67dd6
5
+ SHA512:
6
+ metadata.gz: b6aacd59a3eaacb5a98b7e7ba084d4b49ba65fe77caae1d227c7fb8469d5e238667fd1c3811d3c778adeca3a6911e04f88fac961d5a2c275ca8a492ea952dd42
7
+ data.tar.gz: c7fae34b0cf744d87f72062ea07ffa7d2508e3f2d9f4c260b89c66054f0b2e016bb2d4ee28c3017fc5b1156b3c0581f18f215de1facbcd3650bcdecda4c58b08
@@ -0,0 +1,108 @@
1
+ require 'curlybars/version'
2
+
3
+ # Curlybars is a view system based on Curly that uses Handlebars syntax.
4
+ #
5
+ # Each view consists of two parts, a template and a presenter.
6
+ # The template is a valid Handlebars template.
7
+ #
8
+ # {{#with invoice}}
9
+ # Hello {{recipient.first_name}},
10
+ # you owe us {{local_currency amount}}.
11
+ # {{/with}}
12
+ #
13
+ # In the example above `recipient.first_name` is a path
14
+ # `local_currency amount` is an helper
15
+ #
16
+ # See Curlybars::Presenter for more information on presenters.
17
+ module Curlybars
18
+ class << self
19
+ # Compiles a Curlybars template to Ruby code.
20
+ #
21
+ # source - The source HBS String that should be compiled.
22
+ # identifier - The the file name of the template being compiled (defaults to `nil`).
23
+ #
24
+ # Returns a String containing the Ruby code.
25
+ def compile(source, identifier = nil)
26
+ transformers = Curlybars.configuration.compiler_transformers
27
+ transformed_source = transformers.inject(source) do |memo, transformer|
28
+ transformer.transform(memo, identifier)
29
+ end
30
+
31
+ ast(transformed_source, identifier, run_processors: true).compile
32
+ end
33
+
34
+ # Validates the source against a presenter.
35
+ #
36
+ # dependency_tree - a presenter dependency tree as defined in Curlybars::MethodWhitelist
37
+ # source - The source HBS String that should be validated.
38
+ # identifier - The the file name of the template being validated (defaults to `nil`).
39
+ #
40
+ # Returns an array of Curlybars::Error::Validation
41
+ def validate(dependency_tree, source, identifier = nil, **options)
42
+ options.reverse_merge!(
43
+ run_processors: true
44
+ )
45
+
46
+ errors = begin
47
+ branches = [dependency_tree]
48
+ ast(source, identifier, run_processors: options[:run_processors]).validate(branches)
49
+ rescue Curlybars::Error::Base => ast_error
50
+ [ast_error]
51
+ end
52
+ errors.flatten!
53
+ errors.compact!
54
+ errors
55
+ end
56
+
57
+ # Check if the source is valid for a given presenter.
58
+ #
59
+ # presenter_class - the presenter class, used to check if the source is valid.
60
+ # source - The source HBS String that should be check to be valid.
61
+ # identifier - The the file name of the template being checked (defaults to `nil`).
62
+ #
63
+ # Returns true if the template is valid, false otherwise.
64
+ def valid?(presenter_class, source, identifier = nil, **options)
65
+ errors = validate(presenter_class, source, identifier, **options)
66
+ errors.empty?
67
+ end
68
+
69
+ private
70
+
71
+ def ast(source, identifier, run_processors:)
72
+ tokens = Curlybars::Lexer.lex(source, identifier)
73
+
74
+ Curlybars::Processor::Tilde.process!(tokens, identifier)
75
+
76
+ if run_processors
77
+ Curlybars.configuration.custom_processors.each do |processor|
78
+ processor.process!(tokens, identifier)
79
+ end
80
+ end
81
+
82
+ Curlybars::Parser.parse(tokens)
83
+ rescue RLTK::LexingError => lexing_error
84
+ raise Curlybars::Error::Lex.new(source, identifier, lexing_error)
85
+ rescue RLTK::NotInLanguage => not_in_language_error
86
+ raise Curlybars::Error::Parse.new(source, not_in_language_error)
87
+ end
88
+ end
89
+ end
90
+
91
+ require 'curlybars/safe_buffer'
92
+ require 'curlybars/configuration'
93
+ require 'curlybars/rendering_support'
94
+ require 'curlybars/parser'
95
+ require 'curlybars/position'
96
+ require 'curlybars/lexer'
97
+ require 'curlybars/parser'
98
+ require 'curlybars/processor/token_factory'
99
+ require 'curlybars/processor/tilde'
100
+ require 'curlybars/error/lex'
101
+ require 'curlybars/error/parse'
102
+ require 'curlybars/error/compile'
103
+ require 'curlybars/error/validate'
104
+ require 'curlybars/error/render'
105
+ require 'curlybars/template_handler'
106
+ require 'curlybars/railtie' if defined?(Rails)
107
+ require 'curlybars/presenter'
108
+ require 'curlybars/method_whitelist'
@@ -0,0 +1,41 @@
1
+ module Curlybars
2
+ class << self
3
+ attr_writer :configuration
4
+ end
5
+
6
+ def self.configuration
7
+ @configuration ||= Configuration.new
8
+ end
9
+
10
+ def self.configure
11
+ yield(configuration)
12
+ end
13
+
14
+ def self.reset
15
+ @configuration = Configuration.new
16
+ end
17
+
18
+ class Configuration
19
+ attr_accessor :presenters_namespace
20
+ attr_accessor :nesting_limit
21
+ attr_accessor :traversing_limit
22
+ attr_accessor :output_limit
23
+ attr_accessor :rendering_timeout
24
+ attr_accessor :custom_processors
25
+ attr_accessor :compiler_transformers
26
+ attr_accessor :global_helpers_provider_classes
27
+ attr_accessor :cache
28
+
29
+ def initialize
30
+ @presenters_namespace = ''
31
+ @nesting_limit = 10
32
+ @traversing_limit = 10
33
+ @output_limit = 1.megabyte
34
+ @rendering_timeout = 10.seconds
35
+ @custom_processors = []
36
+ @compiler_transformers = []
37
+ @global_helpers_provider_classes = []
38
+ @cache = ->(key, &block) { block.call }
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,8 @@
1
+ module Curlybars
2
+ class DependencyTracker
3
+ def self.call(path, template)
4
+ presenter = Curlybars::Presenter.presenter_for_path(path)
5
+ presenter.dependencies.to_a
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,18 @@
1
+ module Curlybars
2
+ module Error
3
+ class Base < StandardError
4
+ attr_reader :id, :position, :metadata
5
+
6
+ def initialize(id, message, position, metadata = {})
7
+ super(message)
8
+ @id = id
9
+ @position = position
10
+ @metadata = metadata
11
+ return if position.nil?
12
+ return if position.file_name.nil?
13
+ location = "%s:%d:%d" % [position.file_name, position.line_number, position.line_offset]
14
+ set_backtrace([location])
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,11 @@
1
+ require 'curlybars/error/base'
2
+
3
+ module Curlybars
4
+ module Error
5
+ class Compile < Curlybars::Error::Base
6
+ def initialize(id, message, position)
7
+ super('compile.%s' % id, message, position)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,22 @@
1
+ require 'curlybars/error/base'
2
+
3
+ module Curlybars
4
+ module Error
5
+ class Lex < Curlybars::Error::Base
6
+ def initialize(source, file_name, exception)
7
+ line_number = exception.line_number
8
+ line_offset = exception.line_offset
9
+
10
+ error_line = source.split("\n")[line_number - 1]
11
+ before_error = error_line.first(line_offset).last(10)
12
+ after_error = error_line[line_offset + 1..-1].first(10)
13
+ error = error_line[line_offset]
14
+
15
+ details = [before_error, error, after_error]
16
+ message = ".. %s `%s` %s .. is not permitted symbol in this context" % details
17
+ position = Curlybars::Position.new(file_name, line_number, line_offset)
18
+ super('lex', message, position)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,41 @@
1
+ require 'curlybars/error/base'
2
+
3
+ module Curlybars
4
+ module Error
5
+ class Parse < Curlybars::Error::Base
6
+ def initialize(source, exception)
7
+ position = exception.current.position
8
+
9
+ if exception.current.type == :EOS
10
+ message = "A block helper hasn't been closed properly"
11
+ position = EOSPosition.new(source)
12
+ else
13
+ line_number = position.line_number
14
+ line_offset = position.line_offset
15
+ length = exception.current.position.length
16
+
17
+ error_line = source.split("\n")[line_number - 1]
18
+ before_error = error_line.first(line_offset).last(10)
19
+ after_error = error_line[line_offset + length..-1].first(10)
20
+ error = error_line.slice(line_offset, length)
21
+
22
+ details = [before_error, error, after_error]
23
+ message = ".. %s `%s` %s .. is not permitted in this context" % details
24
+ end
25
+
26
+ super('parse', message, position)
27
+ end
28
+ end
29
+
30
+ class EOSPosition
31
+ attr_reader :line_number, :line_offset, :length, :file_name
32
+
33
+ def initialize(source)
34
+ @line_number = source.count("\n") + 1
35
+ @line_offset = 0
36
+ @length = source.rpartition("\n").last.length
37
+ @file_name = nil
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,23 @@
1
+ module Curlybars
2
+ module Error
3
+ module Presenter
4
+ class NotFound < StandardError
5
+ attr_reader :path
6
+
7
+ def initialize(path)
8
+ @path = path
9
+ end
10
+
11
+ def message
12
+ "error compiling `#{path}`: could not find #{presenter_class_name}"
13
+ end
14
+
15
+ private
16
+
17
+ def presenter_class_name
18
+ Curlybars::Presenter.presenter_name_for_path(path)
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,11 @@
1
+ require 'curlybars/error/base'
2
+
3
+ module Curlybars
4
+ module Error
5
+ class Render < Curlybars::Error::Base
6
+ def initialize(id, message, position, **metadata)
7
+ super('render.%s' % id, message, position, metadata)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,18 @@
1
+ require 'curlybars/error/base'
2
+
3
+ module Curlybars
4
+ module Error
5
+ class Validate < Curlybars::Error::Base
6
+ def initialize(id, message, rltk_position, offset_adjustment = 0, **metadata)
7
+ position = Curlybars::Position.new(
8
+ rltk_position.file_name,
9
+ rltk_position.line_number,
10
+ rltk_position.line_offset + offset_adjustment,
11
+ rltk_position.length - offset_adjustment
12
+ )
13
+
14
+ super('validate.%s' % id, message, position, metadata)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,60 @@
1
+ require 'rltk/lexer'
2
+
3
+ # rubocop:disable Style/RegexpLiteral, Style/Semicolon
4
+ module Curlybars
5
+ class Lexer < RLTK::Lexer
6
+ match_first
7
+
8
+ # The following is an identifier Handlebars compliant
9
+ # IDENTIFIER = '[A-Za-z_][0-9\w]*'
10
+
11
+ # This accomodates the edge case of identifiers containing dashes
12
+ # IDENTIFIER = '[A-Za-z_\-][0-9\w\-]*'
13
+
14
+ # This accomodates the edge case of identifiers containing all numbers
15
+ # and dashes
16
+ IDENTIFIER = '[0-9A-Za-z_\-][0-9\w\-]*'.freeze
17
+
18
+ r(/\\{/) { [:TEXT, '{'] }
19
+ r(/\\/) { [:TEXT, '\\'] }
20
+
21
+ r(/{{!--/) { push_state :comment_block }
22
+ r(/--}}/, :comment_block) { pop_state }
23
+ r(/./m, :comment_block)
24
+
25
+ r(/{{!/) { push_state :comment }
26
+ r(/}}/, :comment) { pop_state }
27
+ r(/./m, :comment)
28
+
29
+ r(/{{~/) { push_state :curly; :TILDE_START }
30
+ r(/~}}/, :curly) { pop_state; :TILDE_END }
31
+
32
+ r(/{{/) { push_state :curly; :START }
33
+ r(/}}/, :curly) { pop_state; :END }
34
+
35
+ r(/#/, :curly) { :HASH }
36
+ r(/\//, :curly) { :SLASH }
37
+ r(/>/, :curly) { :GT }
38
+
39
+ r(/if\b/, :curly) { :IF }
40
+ r(/unless\b/, :curly) { :UNLESS }
41
+ r(/each\b/, :curly) { :EACH }
42
+ r(/with\b/, :curly) { :WITH }
43
+ r(/else\b/, :curly) { :ELSE }
44
+
45
+ r(/true/, :curly) { [:LITERAL, true] }
46
+ r(/false/, :curly) { [:LITERAL, false] }
47
+ r(/[-+]?\d+/, :curly) { |integer| [:LITERAL, integer.to_i] }
48
+ r(/'(.*?)'/, :curly) { [:LITERAL, match[1].inspect] }
49
+ r(/"(.*?)"/, :curly) { [:LITERAL, match[1].inspect] }
50
+
51
+ r(/@((?:\.\.\/)*#{IDENTIFIER})/, :curly) { |variable| [:VARIABLE, match[1]] }
52
+
53
+ r(/(#{IDENTIFIER})\s*=/, :curly) { [:KEY, match[1]] }
54
+ r(/(?:\.\.\/)*(?:#{IDENTIFIER}\.)*#{IDENTIFIER}/, :curly) { |name| [:PATH, name] }
55
+
56
+ r(/\s/, :curly)
57
+
58
+ r(/.*?(?=\\|{{|\z)/m) { |text| [:TEXT, text] }
59
+ end
60
+ end
@@ -0,0 +1,69 @@
1
+ module Curlybars
2
+ module MethodWhitelist
3
+ def allow_methods(*methods, **methods_with_type)
4
+ methods_with_type.each do |(method_name, type)|
5
+ if type.is_a?(Array)
6
+ if type.size != 1 || !type.first.respond_to?(:dependency_tree)
7
+ raise "Invalid allowed method syntax for `#{method_name}`. Collections must be of one presenter class"
8
+ end
9
+ end
10
+ end
11
+
12
+ define_method(:allowed_methods) do
13
+ methods_list = methods + methods_with_type.keys
14
+ defined?(super) ? super() + methods_list : methods_list
15
+ end
16
+
17
+ define_singleton_method(:methods_schema) do |*args|
18
+ schema = methods.each_with_object({}) do |method, memo|
19
+ memo[method] = nil
20
+ end
21
+
22
+ methods_with_type_resolved = methods_with_type.each_with_object({}) do |(method_name, type), memo|
23
+ memo[method_name] = if type.respond_to?(:call)
24
+ type.call(*args)
25
+ else
26
+ type
27
+ end
28
+ end
29
+
30
+ schema.merge!(methods_with_type_resolved)
31
+
32
+ # Inheritance
33
+ schema.merge!(super(*args)) if defined?(super)
34
+
35
+ # Included modules
36
+ included_modules.each do |mod|
37
+ next unless mod.respond_to?(:methods_schema)
38
+ schema.merge!(mod.methods_schema(*args))
39
+ end
40
+
41
+ schema
42
+ end
43
+
44
+ define_singleton_method(:dependency_tree) do |*args|
45
+ methods_schema(*args).each_with_object({}) do |method_with_type, memo|
46
+ method_name = method_with_type.first
47
+ type = method_with_type.last
48
+
49
+ memo[method_name] = if type.respond_to?(:dependency_tree)
50
+ type.dependency_tree(*args)
51
+ elsif type.is_a?(Array)
52
+ [type.first.dependency_tree(*args)]
53
+ else
54
+ type
55
+ end
56
+ end
57
+ end
58
+
59
+ define_method(:allows_method?) do |method|
60
+ allowed_methods.include?(method)
61
+ end
62
+ end
63
+
64
+ def self.extended(base)
65
+ # define a default of no method allowed
66
+ base.allow_methods
67
+ end
68
+ end
69
+ end