scribble 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +22 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE.txt +22 -0
  5. data/README.md +455 -0
  6. data/Rakefile +2 -0
  7. data/lib/scribble.rb +44 -0
  8. data/lib/scribble/block.rb +25 -0
  9. data/lib/scribble/converter.rb +10 -0
  10. data/lib/scribble/errors.rb +24 -0
  11. data/lib/scribble/method.rb +91 -0
  12. data/lib/scribble/methods/if.rb +26 -0
  13. data/lib/scribble/methods/layout.rb +25 -0
  14. data/lib/scribble/methods/partial.rb +14 -0
  15. data/lib/scribble/methods/times.rb +11 -0
  16. data/lib/scribble/nodes/call.rb +55 -0
  17. data/lib/scribble/nodes/ending.rb +6 -0
  18. data/lib/scribble/nodes/node.rb +24 -0
  19. data/lib/scribble/nodes/value.rb +16 -0
  20. data/lib/scribble/objects/boolean.rb +33 -0
  21. data/lib/scribble/objects/fixnum.rb +53 -0
  22. data/lib/scribble/objects/nil.rb +21 -0
  23. data/lib/scribble/objects/string.rb +62 -0
  24. data/lib/scribble/parsing/nester.rb +49 -0
  25. data/lib/scribble/parsing/parser.rb +132 -0
  26. data/lib/scribble/parsing/reporter.rb +71 -0
  27. data/lib/scribble/parsing/transform.rb +87 -0
  28. data/lib/scribble/partial.rb +41 -0
  29. data/lib/scribble/registry.rb +95 -0
  30. data/lib/scribble/support/context.rb +98 -0
  31. data/lib/scribble/support/matcher.rb +74 -0
  32. data/lib/scribble/support/unmatched.rb +70 -0
  33. data/lib/scribble/support/utilities.rb +49 -0
  34. data/lib/scribble/template.rb +61 -0
  35. data/lib/scribble/version.rb +3 -0
  36. data/scribble.gemspec +22 -0
  37. data/test/all.rb +23 -0
  38. data/test/errors_test.rb +94 -0
  39. data/test/methods/if_test.rb +49 -0
  40. data/test/methods/layout_test.rb +71 -0
  41. data/test/methods/partial_test.rb +85 -0
  42. data/test/methods/times_test.rb +10 -0
  43. data/test/objects/boolean_test.rb +162 -0
  44. data/test/objects/fixnum_test.rb +236 -0
  45. data/test/objects/nil_test.rb +83 -0
  46. data/test/objects/string_test.rb +268 -0
  47. data/test/parsing/parsing_test.rb +234 -0
  48. data/test/registry_test.rb +264 -0
  49. data/test/template_test.rb +51 -0
  50. data/test/test_helper.rb +65 -0
  51. 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