scribble 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +22 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +455 -0
- data/Rakefile +2 -0
- data/lib/scribble.rb +44 -0
- data/lib/scribble/block.rb +25 -0
- data/lib/scribble/converter.rb +10 -0
- data/lib/scribble/errors.rb +24 -0
- data/lib/scribble/method.rb +91 -0
- data/lib/scribble/methods/if.rb +26 -0
- data/lib/scribble/methods/layout.rb +25 -0
- data/lib/scribble/methods/partial.rb +14 -0
- data/lib/scribble/methods/times.rb +11 -0
- data/lib/scribble/nodes/call.rb +55 -0
- data/lib/scribble/nodes/ending.rb +6 -0
- data/lib/scribble/nodes/node.rb +24 -0
- data/lib/scribble/nodes/value.rb +16 -0
- data/lib/scribble/objects/boolean.rb +33 -0
- data/lib/scribble/objects/fixnum.rb +53 -0
- data/lib/scribble/objects/nil.rb +21 -0
- data/lib/scribble/objects/string.rb +62 -0
- data/lib/scribble/parsing/nester.rb +49 -0
- data/lib/scribble/parsing/parser.rb +132 -0
- data/lib/scribble/parsing/reporter.rb +71 -0
- data/lib/scribble/parsing/transform.rb +87 -0
- data/lib/scribble/partial.rb +41 -0
- data/lib/scribble/registry.rb +95 -0
- data/lib/scribble/support/context.rb +98 -0
- data/lib/scribble/support/matcher.rb +74 -0
- data/lib/scribble/support/unmatched.rb +70 -0
- data/lib/scribble/support/utilities.rb +49 -0
- data/lib/scribble/template.rb +61 -0
- data/lib/scribble/version.rb +3 -0
- data/scribble.gemspec +22 -0
- data/test/all.rb +23 -0
- data/test/errors_test.rb +94 -0
- data/test/methods/if_test.rb +49 -0
- data/test/methods/layout_test.rb +71 -0
- data/test/methods/partial_test.rb +85 -0
- data/test/methods/times_test.rb +10 -0
- data/test/objects/boolean_test.rb +162 -0
- data/test/objects/fixnum_test.rb +236 -0
- data/test/objects/nil_test.rb +83 -0
- data/test/objects/string_test.rb +268 -0
- data/test/parsing/parsing_test.rb +234 -0
- data/test/registry_test.rb +264 -0
- data/test/template_test.rb +51 -0
- data/test/test_helper.rb +65 -0
- metadata +127 -0
@@ -0,0 +1,95 @@
|
|
1
|
+
module Scribble
|
2
|
+
class Registry
|
3
|
+
|
4
|
+
# Singleton
|
5
|
+
|
6
|
+
def self.method_missing name, *args, &proc
|
7
|
+
instance.send name, *args, &proc
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.instance
|
11
|
+
@instance ||= Registry.new
|
12
|
+
end
|
13
|
+
|
14
|
+
# For class context
|
15
|
+
|
16
|
+
def for *classes, &proc
|
17
|
+
classes.each { |receiver_class| ForClassContext.new(self, receiver_class).instance_eval &proc }
|
18
|
+
end
|
19
|
+
|
20
|
+
class ForClassContext < BasicObject
|
21
|
+
def initialize registry, receiver_class
|
22
|
+
@registry, @receiver_class = registry, receiver_class
|
23
|
+
end
|
24
|
+
|
25
|
+
def method name, *signature, block: false, **options
|
26
|
+
(block ? Block : Method).implement @receiver_class, name, signature, @registry, **options
|
27
|
+
end
|
28
|
+
|
29
|
+
def to_boolean &proc
|
30
|
+
method :to_boolean, to: proc
|
31
|
+
end
|
32
|
+
|
33
|
+
def to_string &proc
|
34
|
+
method :to_string, to: proc
|
35
|
+
end
|
36
|
+
|
37
|
+
def name name
|
38
|
+
@registry.class_name @receiver_class, name
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Class names
|
43
|
+
|
44
|
+
def class_names
|
45
|
+
@class_names ||= {}
|
46
|
+
end
|
47
|
+
|
48
|
+
def class_name receiver_class, name = nil
|
49
|
+
class_names[receiver_class] = name if name
|
50
|
+
class_names[receiver_class] || receiver_class.name.downcase
|
51
|
+
end
|
52
|
+
|
53
|
+
# Methods
|
54
|
+
|
55
|
+
def methods
|
56
|
+
@methods ||= []
|
57
|
+
end
|
58
|
+
|
59
|
+
def unregister name
|
60
|
+
methods.reject! { |method| method.name == name }
|
61
|
+
end
|
62
|
+
|
63
|
+
# Evaluate or cast
|
64
|
+
|
65
|
+
def evaluate name, receiver, args, call = nil, context = nil
|
66
|
+
matcher = Support::Matcher.new self, name, receiver, args
|
67
|
+
matcher.match.new(receiver, call, context).send name, *args
|
68
|
+
end
|
69
|
+
|
70
|
+
def to_boolean receiver
|
71
|
+
evaluate :to_boolean, receiver, []
|
72
|
+
end
|
73
|
+
|
74
|
+
def to_string receiver
|
75
|
+
evaluate :to_string, receiver, []
|
76
|
+
end
|
77
|
+
|
78
|
+
# Split, block
|
79
|
+
|
80
|
+
def split? name
|
81
|
+
method_predicate name, :split?
|
82
|
+
end
|
83
|
+
|
84
|
+
def block? name
|
85
|
+
method_predicate name, :block?
|
86
|
+
end
|
87
|
+
|
88
|
+
private
|
89
|
+
|
90
|
+
def method_predicate name, predicate
|
91
|
+
method = methods.find { |method| method.method_name == name }
|
92
|
+
method && method.send(predicate)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
module Scribble
|
2
|
+
module Support
|
3
|
+
module Context
|
4
|
+
|
5
|
+
# Registry method shortcut
|
6
|
+
|
7
|
+
module ClassMethods
|
8
|
+
def method *args
|
9
|
+
Scribble::Registry.for(self) { method *args }
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.included base
|
14
|
+
base.extend ClassMethods
|
15
|
+
end
|
16
|
+
|
17
|
+
# Rendering
|
18
|
+
|
19
|
+
def nodes
|
20
|
+
raise NotImplementedError, 'Class that includes context must implement nodes method'
|
21
|
+
end
|
22
|
+
|
23
|
+
def render nodes: nodes, context: self
|
24
|
+
if !require_conversion?
|
25
|
+
render_without_conversion nodes, context
|
26
|
+
|
27
|
+
elsif converter
|
28
|
+
converter.convert render_without_conversion(nodes, context)
|
29
|
+
|
30
|
+
elsif format.nil?
|
31
|
+
raise "Cannot convert to #{render_format} without format"
|
32
|
+
else
|
33
|
+
raise "No suitable converter converting #{format} to #{render_format}"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def render_without_conversion nodes, context
|
38
|
+
nodes.map { |node| Scribble::Registry.to_string node.evaluate(context) }.join
|
39
|
+
end
|
40
|
+
|
41
|
+
# Template and registers
|
42
|
+
|
43
|
+
def template
|
44
|
+
@context.template
|
45
|
+
end
|
46
|
+
|
47
|
+
def registers
|
48
|
+
@context.registers
|
49
|
+
end
|
50
|
+
|
51
|
+
# Format conversion
|
52
|
+
|
53
|
+
def format
|
54
|
+
@context.format
|
55
|
+
end
|
56
|
+
|
57
|
+
def render_format
|
58
|
+
@context.format
|
59
|
+
end
|
60
|
+
|
61
|
+
def require_conversion?
|
62
|
+
render_format && format != render_format
|
63
|
+
end
|
64
|
+
|
65
|
+
def converter
|
66
|
+
@converter ||= template.find_converter(format, render_format)
|
67
|
+
end
|
68
|
+
|
69
|
+
# Variables
|
70
|
+
|
71
|
+
def variables
|
72
|
+
@variables ||= {}
|
73
|
+
end
|
74
|
+
|
75
|
+
def set_variable name, value
|
76
|
+
variables[name] = value
|
77
|
+
end
|
78
|
+
|
79
|
+
def resolve_variable call
|
80
|
+
variables[call.name] if call.allow_variable
|
81
|
+
end
|
82
|
+
|
83
|
+
# Evaluation
|
84
|
+
|
85
|
+
def evaluate call, args, context
|
86
|
+
resolve_variable(call) || Scribble::Registry.evaluate(call.name, self, args, call, context)
|
87
|
+
rescue Support::Unmatched => local
|
88
|
+
raise local if @context.nil? || call.split?
|
89
|
+
|
90
|
+
begin
|
91
|
+
@context.evaluate call, args, context
|
92
|
+
rescue Support::Unmatched => global
|
93
|
+
raise global.merge(local)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module Scribble
|
2
|
+
module Support
|
3
|
+
class Matcher
|
4
|
+
def initialize registry, name, receiver, args
|
5
|
+
@registry, @name, @receiver, @args = registry, name, receiver, args
|
6
|
+
end
|
7
|
+
|
8
|
+
# Name and receiver matching
|
9
|
+
|
10
|
+
def base_matches
|
11
|
+
@base_matches ||= @registry.methods.select do |method|
|
12
|
+
method.method_name == @name && @receiver.is_a?(method.receiver_class)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
# Args cursor helpers
|
17
|
+
|
18
|
+
def step_arg arg_class
|
19
|
+
@cursor += 1 if @cursor < @args.size && @args[@cursor].is_a?(arg_class)
|
20
|
+
end
|
21
|
+
|
22
|
+
def step_args arg_class, max = nil
|
23
|
+
index = 0; while index += 1
|
24
|
+
break unless step_arg arg_class
|
25
|
+
break if max && index == max
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# Argument expectations
|
30
|
+
|
31
|
+
def expect arg_class
|
32
|
+
if @max_cursor.nil? || @cursor > @max_cursor
|
33
|
+
@expected, @max_cursor = [], @cursor
|
34
|
+
end
|
35
|
+
if arg_class && @cursor == @max_cursor
|
36
|
+
@expected << @registry.class_name(arg_class)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def unexpected
|
41
|
+
if @max_cursor && @max_cursor < @args.size
|
42
|
+
@registry.class_name @args[@max_cursor].class
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Match a single method
|
47
|
+
|
48
|
+
def match_args method
|
49
|
+
@cursor = 0
|
50
|
+
method.signature.each do |element|
|
51
|
+
if element.is_a? Array
|
52
|
+
step_args *element
|
53
|
+
elsif !step_arg element
|
54
|
+
expect element; return false
|
55
|
+
end
|
56
|
+
end
|
57
|
+
if @cursor < @args.size
|
58
|
+
expect nil; return false
|
59
|
+
end
|
60
|
+
method
|
61
|
+
end
|
62
|
+
|
63
|
+
# Get first match
|
64
|
+
|
65
|
+
def match
|
66
|
+
@match ||= base_matches.select do |method|
|
67
|
+
@args.size >= method.min_arity && (method.max_arity.nil? || @args.size <= method.max_arity)
|
68
|
+
end.select do |method|
|
69
|
+
match_args method
|
70
|
+
end.first || raise(Unmatched.new @base_matches, @expected || [], unexpected, @max_cursor)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module Scribble
|
2
|
+
module Support
|
3
|
+
class Unmatched < Exception
|
4
|
+
def initialize base_matches, expected, unexpected, max_cursor
|
5
|
+
@base_matches, @expected, @unexpected, @max_cursor =
|
6
|
+
base_matches, expected, unexpected, max_cursor
|
7
|
+
end
|
8
|
+
|
9
|
+
# Error message helpers
|
10
|
+
|
11
|
+
def human_arities
|
12
|
+
@base_matches.map do |method|
|
13
|
+
[method.min_arity, method.max_arity]
|
14
|
+
end.uniq.map do |min, max|
|
15
|
+
max ? (min == max ? min.to_s : "#{min}-#{max}") : "#{min}+"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def arg_to call
|
20
|
+
"#{Support::Utilities.ordinalize @max_cursor + 1} argument to '#{call.name}'"
|
21
|
+
end
|
22
|
+
|
23
|
+
def expected_sentence
|
24
|
+
"Expected #{Support::Utilities.to_sentence @expected}"
|
25
|
+
end
|
26
|
+
|
27
|
+
def unexpected_sentence call
|
28
|
+
"got #{@unexpected} #{call.args[@max_cursor].line_and_column}"
|
29
|
+
end
|
30
|
+
|
31
|
+
# To error
|
32
|
+
|
33
|
+
def to_error call
|
34
|
+
if @base_matches.empty?
|
35
|
+
Errors::Undefined.new "Undefined #{'variable or ' if call.allow_variable}"\
|
36
|
+
"method '#{call.name}' #{call.line_and_column}"
|
37
|
+
elsif @expected.empty? && @unexpected.nil?
|
38
|
+
Errors::Arity.new "Wrong number of arguments (#{call.args.size} "\
|
39
|
+
"for #{Support::Utilities.to_sentence human_arities}) "\
|
40
|
+
"for '#{call.name}' #{call.line_and_column}"
|
41
|
+
elsif @expected.empty?
|
42
|
+
Errors::Argument.new "Unexpected #{arg_to call}, #{unexpected_sentence call}"
|
43
|
+
elsif @unexpected.nil?
|
44
|
+
Errors::Argument.new "#{expected_sentence} as #{arg_to call} #{call.line_and_column}"
|
45
|
+
else
|
46
|
+
Errors::Argument.new "#{expected_sentence} as #{arg_to call}, #{unexpected_sentence call}"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Merge unmatched exceptions to generate merged error
|
51
|
+
|
52
|
+
attr_reader :base_matches, :expected, :unexpected, :max_cursor
|
53
|
+
|
54
|
+
def merge unmatched
|
55
|
+
@base_matches += unmatched.base_matches
|
56
|
+
|
57
|
+
unless @max_cursor.nil?
|
58
|
+
if unmatched.max_cursor > @max_cursor
|
59
|
+
@max_cursor = unmatched.max_cursor
|
60
|
+
@expected = unmatched.expected
|
61
|
+
@unexpected = unmatched.unexpected
|
62
|
+
elsif unmatched.max_cursor == @max_cursor
|
63
|
+
@expected += unmatched.expected
|
64
|
+
end
|
65
|
+
end
|
66
|
+
self
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module Scribble
|
2
|
+
module Support
|
3
|
+
class Utilities
|
4
|
+
class << self
|
5
|
+
|
6
|
+
# String repetition
|
7
|
+
|
8
|
+
def repeat string, count
|
9
|
+
raise Errors::UnlocatedArgument.new("Can't repeat string a negative number of times") if count < 0
|
10
|
+
string * count
|
11
|
+
end
|
12
|
+
|
13
|
+
# String truncation
|
14
|
+
|
15
|
+
def truncate string, on_words, length, omission
|
16
|
+
raise Errors::UnlocatedArgument.new("Can't truncate string with a negative length") if length < 0
|
17
|
+
|
18
|
+
truncated = if on_words
|
19
|
+
string[/(\s*\S*){#{length}}/]
|
20
|
+
else
|
21
|
+
string[0, length]
|
22
|
+
end
|
23
|
+
|
24
|
+
if string != truncated then truncated + omission else string end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Array to sentence
|
28
|
+
|
29
|
+
def to_sentence strings, final_separator = ' or '
|
30
|
+
[strings[0..-2].join(', '), strings[-1].to_s].reject(&:empty?).join(final_separator)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Ordinalize number
|
34
|
+
def ordinalize number
|
35
|
+
if (11..13).include?(number % 100)
|
36
|
+
"#{number}th"
|
37
|
+
else
|
38
|
+
case number % 10
|
39
|
+
when 1; "#{number}st"
|
40
|
+
when 2; "#{number}nd"
|
41
|
+
when 3; "#{number}rd"
|
42
|
+
else "#{number}th"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module Scribble
|
2
|
+
class Template < Partial
|
3
|
+
|
4
|
+
def initialize source, format: nil, loader: nil, converters: []
|
5
|
+
super source, format: format
|
6
|
+
@loader, @converters = loader, converters
|
7
|
+
end
|
8
|
+
|
9
|
+
# Template context
|
10
|
+
|
11
|
+
class Context < Partial::Context
|
12
|
+
def initialize template, registers, variables, render_format
|
13
|
+
@template, @registers, @variables, @render_format = template, registers, variables, render_format
|
14
|
+
end
|
15
|
+
|
16
|
+
def nodes
|
17
|
+
@template.nodes
|
18
|
+
end
|
19
|
+
|
20
|
+
def template
|
21
|
+
@template
|
22
|
+
end
|
23
|
+
|
24
|
+
def registers
|
25
|
+
@registers
|
26
|
+
end
|
27
|
+
|
28
|
+
def format
|
29
|
+
@template.format
|
30
|
+
end
|
31
|
+
|
32
|
+
def render_format
|
33
|
+
@render_format
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Render
|
38
|
+
|
39
|
+
def render variables: {}, registers: {}, format: nil
|
40
|
+
Context.new(self, registers, variables, format).render
|
41
|
+
end
|
42
|
+
|
43
|
+
# Load partial
|
44
|
+
|
45
|
+
def load name, context
|
46
|
+
if @loader
|
47
|
+
if partial = @loader.load(name)
|
48
|
+
Partial::Context.new partial, context
|
49
|
+
end
|
50
|
+
else
|
51
|
+
raise 'Cannot load partial without loader'
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# Find converter
|
56
|
+
|
57
|
+
def find_converter from, to
|
58
|
+
@converters.find { |converter| converter.from == from && converter.to == to }
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|