curlybars 0.9.13

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 (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