scribble 1.0.0
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/.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
|