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