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