curlybars 0.9.13
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/lib/curlybars.rb +108 -0
- data/lib/curlybars/configuration.rb +41 -0
- data/lib/curlybars/dependency_tracker.rb +8 -0
- data/lib/curlybars/error/base.rb +18 -0
- data/lib/curlybars/error/compile.rb +11 -0
- data/lib/curlybars/error/lex.rb +22 -0
- data/lib/curlybars/error/parse.rb +41 -0
- data/lib/curlybars/error/presenter/not_found.rb +23 -0
- data/lib/curlybars/error/render.rb +11 -0
- data/lib/curlybars/error/validate.rb +18 -0
- data/lib/curlybars/lexer.rb +60 -0
- data/lib/curlybars/method_whitelist.rb +69 -0
- data/lib/curlybars/node/block_helper_else.rb +108 -0
- data/lib/curlybars/node/boolean.rb +24 -0
- data/lib/curlybars/node/each_else.rb +69 -0
- data/lib/curlybars/node/if_else.rb +33 -0
- data/lib/curlybars/node/item.rb +31 -0
- data/lib/curlybars/node/literal.rb +28 -0
- data/lib/curlybars/node/option.rb +25 -0
- data/lib/curlybars/node/output.rb +24 -0
- data/lib/curlybars/node/partial.rb +24 -0
- data/lib/curlybars/node/path.rb +137 -0
- data/lib/curlybars/node/root.rb +29 -0
- data/lib/curlybars/node/string.rb +24 -0
- data/lib/curlybars/node/template.rb +32 -0
- data/lib/curlybars/node/text.rb +24 -0
- data/lib/curlybars/node/unless_else.rb +33 -0
- data/lib/curlybars/node/variable.rb +34 -0
- data/lib/curlybars/node/with_else.rb +54 -0
- data/lib/curlybars/parser.rb +183 -0
- data/lib/curlybars/position.rb +7 -0
- data/lib/curlybars/presenter.rb +288 -0
- data/lib/curlybars/processor/tilde.rb +31 -0
- data/lib/curlybars/processor/token_factory.rb +9 -0
- data/lib/curlybars/railtie.rb +18 -0
- data/lib/curlybars/rendering_support.rb +222 -0
- data/lib/curlybars/safe_buffer.rb +11 -0
- data/lib/curlybars/template_handler.rb +93 -0
- data/lib/curlybars/version.rb +3 -0
- data/spec/acceptance/application_layout_spec.rb +60 -0
- data/spec/acceptance/collection_blocks_spec.rb +28 -0
- data/spec/acceptance/global_helper_spec.rb +25 -0
- data/spec/curlybars/configuration_spec.rb +57 -0
- data/spec/curlybars/error/base_spec.rb +41 -0
- data/spec/curlybars/error/compile_spec.rb +19 -0
- data/spec/curlybars/error/lex_spec.rb +25 -0
- data/spec/curlybars/error/parse_spec.rb +74 -0
- data/spec/curlybars/error/render_spec.rb +19 -0
- data/spec/curlybars/error/validate_spec.rb +19 -0
- data/spec/curlybars/lexer_spec.rb +466 -0
- data/spec/curlybars/method_whitelist_spec.rb +168 -0
- data/spec/curlybars/processor/tilde_spec.rb +60 -0
- data/spec/curlybars/rendering_support_spec.rb +426 -0
- data/spec/curlybars/safe_buffer_spec.rb +46 -0
- data/spec/curlybars/template_handler_spec.rb +222 -0
- data/spec/integration/cache_spec.rb +124 -0
- data/spec/integration/comment_spec.rb +60 -0
- data/spec/integration/exception_spec.rb +31 -0
- data/spec/integration/node/block_helper_else_spec.rb +422 -0
- data/spec/integration/node/each_else_spec.rb +204 -0
- data/spec/integration/node/each_spec.rb +291 -0
- data/spec/integration/node/escape_spec.rb +27 -0
- data/spec/integration/node/helper_spec.rb +176 -0
- data/spec/integration/node/if_else_spec.rb +129 -0
- data/spec/integration/node/if_spec.rb +143 -0
- data/spec/integration/node/output_spec.rb +68 -0
- data/spec/integration/node/partial_spec.rb +66 -0
- data/spec/integration/node/path_spec.rb +286 -0
- data/spec/integration/node/root_spec.rb +15 -0
- data/spec/integration/node/template_spec.rb +86 -0
- data/spec/integration/node/unless_else_spec.rb +129 -0
- data/spec/integration/node/unless_spec.rb +130 -0
- data/spec/integration/node/with_spec.rb +116 -0
- data/spec/integration/processor/tilde_spec.rb +38 -0
- data/spec/integration/processors_spec.rb +30 -0
- metadata +358 -0
checksums.yaml
ADDED
@@ -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
|
data/lib/curlybars.rb
ADDED
@@ -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,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,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,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
|