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,2 @@
1
+ require "bundler/gem_tasks"
2
+
@@ -0,0 +1,44 @@
1
+ # Support
2
+
3
+ require_relative 'scribble/support/context'
4
+ require_relative 'scribble/support/utilities'
5
+ require_relative 'scribble/support/matcher'
6
+ require_relative 'scribble/support/unmatched'
7
+
8
+ # Parsing
9
+
10
+ require_relative 'scribble/parsing/parser'
11
+ require_relative 'scribble/parsing/transform'
12
+ require_relative 'scribble/parsing/nester'
13
+ require_relative 'scribble/parsing/reporter'
14
+
15
+ # Node types
16
+
17
+ require_relative 'scribble/nodes/node'
18
+ require_relative 'scribble/nodes/call'
19
+ require_relative 'scribble/nodes/ending'
20
+ require_relative 'scribble/nodes/value'
21
+
22
+ # Main components
23
+
24
+ require_relative 'scribble/registry'
25
+ require_relative 'scribble/method'
26
+ require_relative 'scribble/block'
27
+ require_relative 'scribble/partial'
28
+ require_relative 'scribble/template'
29
+ require_relative 'scribble/errors'
30
+ require_relative 'scribble/converter'
31
+
32
+ # Object registrations
33
+
34
+ require_relative 'scribble/objects/boolean'
35
+ require_relative 'scribble/objects/fixnum'
36
+ require_relative 'scribble/objects/nil'
37
+ require_relative 'scribble/objects/string'
38
+
39
+ # Method registrations
40
+
41
+ require_relative 'scribble/methods/if'
42
+ require_relative 'scribble/methods/layout'
43
+ require_relative 'scribble/methods/partial'
44
+ require_relative 'scribble/methods/times'
@@ -0,0 +1,25 @@
1
+ module Scribble
2
+ class Block < Method
3
+ include Support::Context
4
+
5
+ def self.block?
6
+ true
7
+ end
8
+
9
+ def nodes
10
+ @nodes || @call.nodes
11
+ end
12
+
13
+ def split_nodes
14
+ nodes.take_while.with_index do |node, index|
15
+ if node.split?
16
+ @nodes = nodes.drop index + 1
17
+ node.evaluate self, allow_split: true, allow_block: false
18
+ false
19
+ else
20
+ true
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,10 @@
1
+ module Scribble
2
+ def self.converter from_to, &block
3
+ Object.new.tap do |converter|
4
+ converter.define_singleton_method(:from) { from_to.keys.first }
5
+ converter.define_singleton_method(:to) { from_to.values.first }
6
+
7
+ converter.define_singleton_method :convert, &block
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,24 @@
1
+ module Scribble
2
+ module Errors
3
+ class Error < RuntimeError
4
+ end
5
+
6
+ class Syntax < Error
7
+ end
8
+
9
+ class Undefined < Error
10
+ end
11
+
12
+ class Arity < Error
13
+ end
14
+
15
+ class Argument < Error
16
+ end
17
+
18
+ class UnlocatedArgument < Error
19
+ end
20
+
21
+ class NotFound < Error
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,91 @@
1
+ module Scribble
2
+ class Method
3
+ def initialize receiver, call, context
4
+ @receiver, @call, @context = receiver, call, context
5
+ end
6
+
7
+ class << self
8
+ attr_reader :method_name, :receiver_class, :signature
9
+
10
+ # Setup instance variables
11
+
12
+ def setup receiver_class, method_name, signature
13
+ raise "Method name needs to be a Symbol, got #{method_name.inspect}" unless method_name.is_a?(Symbol)
14
+ @receiver_class, @method_name, @signature = receiver_class, method_name, signature
15
+ end
16
+
17
+ # Insert method into a registry
18
+
19
+ def eql? other
20
+ receiver_class == other.receiver_class && method_name == other.method_name && signature == other.signature
21
+ end
22
+
23
+ def insert registry
24
+ raise "Duplicate method #{method_name} on #{receiver_class}" if registry.methods.any? { |method| eql? method }
25
+ raise "Method #{method_name} must be a #{'non-' if split?}split method" unless [nil, split?].include? registry.split?(method_name)
26
+ raise "Method #{method_name} must be a #{'non-' if block?}block method" unless [nil, block?].include? registry.block?(method_name)
27
+ registry.methods << self
28
+ end
29
+
30
+ # Setup method and insert into default registry
31
+
32
+ def register method_name, *signature, on: Template::Context
33
+ setup on, method_name, signature
34
+ insert Registry.instance
35
+ end
36
+
37
+ # Subclass, setup and insert into registry
38
+
39
+ def implement receiver_class, method_name, signature, registry, as: nil, to: nil, cast: nil, returns: nil, split: nil
40
+ Class.new(self) do
41
+ setup receiver_class, method_name, signature
42
+
43
+ raise "Received multiple implementation options for method" unless [as, to, cast, returns].compact.size <= 1
44
+ raise "Method option :as requires String, got #{as.inspect}" unless as.nil? || as.is_a?(String)
45
+ raise "Method option :to requires Proc, got #{to.inspect}" unless to.nil? || to.is_a?(Proc)
46
+ raise "Method option :cast must be 'to_boolean' or 'to_string'" unless [nil, 'to_boolean', 'to_string'].include? cast
47
+ raise "Method option :split must be true" unless [nil, true].include? split
48
+
49
+ # Redefine split?
50
+ define_singleton_method :split? do
51
+ true
52
+ end if split
53
+
54
+ # Implement method
55
+ send :define_method, method_name do |*args|
56
+ if to
57
+ @receiver.instance_exec *args, &to
58
+ elsif cast
59
+ registry.evaluate method_name, registry.send(cast, @receiver), args, @call, @context
60
+ elsif !returns.nil?
61
+ returns
62
+ else
63
+ @receiver.send (as || method_name), *args
64
+ end
65
+ end
66
+ end.insert registry
67
+ end
68
+
69
+ # Default split and block
70
+
71
+ def split?; false; end
72
+ def block?; false; end
73
+
74
+ # Arity
75
+
76
+ def min_arity
77
+ @min_arity ||= signature.reduce 0 do |min_arity, element|
78
+ element.is_a?(Array) ? min_arity : min_arity + 1
79
+ end
80
+ end
81
+
82
+ def max_arity
83
+ @max_arity ||= signature.reduce 0 do |max_arity, element|
84
+ if max_arity
85
+ element.is_a?(Array) ? element[1] && max_arity + element[1] : max_arity + 1
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,26 @@
1
+ module Scribble
2
+ module Methods
3
+ class If < Block
4
+ register :if, Object
5
+
6
+ def if object
7
+ @paths = []
8
+ send :elsif, object
9
+
10
+ render(nodes: @paths.map { |condition, nodes| nodes if condition }.compact.first || [])
11
+ end
12
+
13
+ method :elsif, Object, split: true
14
+
15
+ def elsif object
16
+ @paths.unshift [Registry.to_boolean(object), split_nodes]
17
+ end
18
+
19
+ method :else, split: true
20
+
21
+ def else
22
+ @paths.unshift [true, nodes]
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,25 @@
1
+ module Scribble
2
+ module Methods
3
+ class Layout < Block
4
+ register :layout, String, [Object, 1]
5
+
6
+ def layout name, object = nil
7
+ template.load(name, self).tap do |partial_context|
8
+ raise Errors::NotFound.new("Layout partial '#{name}' not found #{@call.line_and_column}") if partial_context.nil?
9
+ partial_context.set_variable name.to_sym, object if object
10
+ @render_format = partial_context.format
11
+ end.render
12
+ end
13
+
14
+ method :content
15
+
16
+ def content
17
+ render context: @context
18
+ end
19
+
20
+ def render_format
21
+ @render_format || super
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,14 @@
1
+ module Scribble
2
+ module Methods
3
+ class Partial < Method
4
+ register :partial, String, [Object, 1]
5
+
6
+ def partial name, object = nil
7
+ @context.template.load(name, @context).tap do |partial_context|
8
+ raise Errors::NotFound.new("Partial '#{name}' not found #{@call.line_and_column}") if partial_context.nil?
9
+ partial_context.set_variable name.to_sym, object if object
10
+ end.render
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,11 @@
1
+ module Scribble
2
+ module Methods
3
+ class Times < Block
4
+ register :times, on: Fixnum
5
+
6
+ def times
7
+ render * @receiver
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,55 @@
1
+ module Scribble
2
+ module Nodes
3
+ class Call < Node
4
+ attr_reader :name, :args, :allow_variable
5
+
6
+ attr_accessor :receiver, :nodes
7
+
8
+ def initialize slice, name, args: [], receiver: nil, allow_variable: false
9
+ super slice
10
+ @name, @args, @receiver, @allow_variable = name, args, receiver, allow_variable
11
+ end
12
+
13
+ def evaluate context, allow_block: true, allow_split: false
14
+ disallow_split unless allow_split
15
+ disallow_block unless allow_block
16
+
17
+ if @receiver
18
+ Registry.evaluate @name, evaluated_receiver(context), evaluated_args(context), self, context
19
+ else
20
+ context.evaluate self, evaluated_args(context), context
21
+ end
22
+ rescue Errors::UnlocatedArgument => e
23
+ raise Errors::Argument.new("#{e.message} #{line_and_column}")
24
+ rescue Support::Unmatched => e
25
+ raise e.to_error self
26
+ end
27
+
28
+ def block?
29
+ Registry.block? name
30
+ end
31
+
32
+ def split?
33
+ Registry.split? name
34
+ end
35
+
36
+ private
37
+
38
+ def disallow_split
39
+ raise Errors::Syntax.new "Unexpected '#{name}' #{line_and_column}" if split?
40
+ end
41
+
42
+ def disallow_block
43
+ raise Errors::Syntax.new "Unexpected '#{name}' #{line_and_column}; block methods can't be arguments" if block?
44
+ end
45
+
46
+ def evaluated_receiver context
47
+ @evaluated_receiver ||= @receiver.evaluate context, allow_block: false
48
+ end
49
+
50
+ def evaluated_args context
51
+ @evaluated_args ||= args.map { |arg| arg.evaluate context, allow_block: false }
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,6 @@
1
+ module Scribble
2
+ module Nodes
3
+ class Ending < Node
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,24 @@
1
+ module Scribble
2
+ module Nodes
3
+ class Node
4
+ attr_reader :slice
5
+
6
+ def initialize slice
7
+ @slice = slice
8
+ end
9
+
10
+ def block?
11
+ false
12
+ end
13
+
14
+ def split?
15
+ false
16
+ end
17
+
18
+ def line_and_column
19
+ line, column = slice.line_and_column
20
+ "at line #{line} column #{column}"
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,16 @@
1
+ module Scribble
2
+ module Nodes
3
+ class Value < Node
4
+ attr_reader :value
5
+
6
+ def initialize slice, value
7
+ super(slice)
8
+ @value = value
9
+ end
10
+
11
+ def evaluate context, **options
12
+ @value
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,33 @@
1
+ module Scribble
2
+ Registry.for TrueClass do
3
+ method :equals, TrueClass, returns: true
4
+ method :equals, FalseClass, returns: false
5
+ method :differs, TrueClass, returns: false
6
+ method :differs, FalseClass, returns: true
7
+ end
8
+
9
+ Registry.for FalseClass do
10
+ method :equals, TrueClass, returns: false
11
+ method :equals, FalseClass, returns: true
12
+ method :differs, TrueClass, returns: true
13
+ method :differs, FalseClass, returns: false
14
+ end
15
+
16
+ Registry.for TrueClass, FalseClass do
17
+ name 'boolean'
18
+
19
+ to_boolean { self }
20
+ to_string { to_s }
21
+
22
+ # Logical operators
23
+ method :or, Object, to: ->(object) { self | Scribble::Registry.to_boolean(object) }
24
+ method :and, Object, to: ->(object) { self & Scribble::Registry.to_boolean(object) }
25
+
26
+ # Equality
27
+ method :equals, Object, returns: false
28
+ method :differs, Object, returns: true
29
+
30
+ # Unary not
31
+ method :not, to: -> { !self }
32
+ end
33
+ end
@@ -0,0 +1,53 @@
1
+ module Scribble
2
+ Registry.for Fixnum do
3
+ name 'number'
4
+
5
+ to_boolean { !zero? }
6
+ to_string { to_s }
7
+
8
+ # Logical operators
9
+ method :or, Object, cast: 'to_boolean'
10
+ method :and, Object, cast: 'to_boolean'
11
+
12
+ # Equality
13
+ method :equals, Fixnum, as: '=='
14
+ method :differs, Fixnum, as: '!='
15
+ method :equals, Object, returns: false
16
+ method :differs, Object, returns: true
17
+
18
+ # Comparisons
19
+ method :greater, Fixnum, as: '>'
20
+ method :greater_or_equal, Fixnum, as: '>='
21
+ method :less, Fixnum, as: '<'
22
+ method :less_or_equal, Fixnum, as: '<='
23
+
24
+ # Calculations
25
+ method :add, Fixnum, as: '+'
26
+ method :subtract, Fixnum, as: '-'
27
+ method :multiply, Fixnum, as: '*'
28
+ method :remainder, Fixnum, as: '%'
29
+
30
+ method :divide, Fixnum, to: ->(n) {
31
+ begin
32
+ self / n
33
+ rescue ZeroDivisionError
34
+ raise Errors::UnlocatedArgument.new "Division by zero"
35
+ end
36
+ }
37
+
38
+ # Unary operators
39
+ method :negative, to: -> { -self }
40
+ method :not, cast: 'to_boolean'
41
+
42
+ # String manipulations
43
+ method :add, String, cast: 'to_string'
44
+ method :multiply, String, to: ->(string) { Support::Utilities.repeat string, self }
45
+
46
+ # Unary methods
47
+ method :abs
48
+ method :odd, as: 'odd?'
49
+ method :even, as: 'even?'
50
+ method :zero, as: 'zero?'
51
+ method :nonzero, to: -> { self != 0 }
52
+ end
53
+ end