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,21 @@
1
+ module Scribble
2
+ Registry.for NilClass do
3
+ name 'nothing'
4
+
5
+ to_boolean { false }
6
+ to_string { '' }
7
+
8
+ # Logical operators
9
+ method :or, Object, cast: 'to_boolean'
10
+ method :and, Object, cast: 'to_boolean'
11
+
12
+ # Equality
13
+ method :equals, NilClass, returns: true
14
+ method :differs, NilClass, returns: false
15
+ method :equals, Object, returns: false
16
+ method :differs, Object, returns: true
17
+
18
+ # Unary operators
19
+ method :not, cast: 'to_boolean'
20
+ end
21
+ end
@@ -0,0 +1,62 @@
1
+ module Scribble
2
+ Registry.for String do
3
+ to_boolean { !empty? }
4
+ to_string { self }
5
+
6
+ # Logical operators
7
+ method :or, Object, cast: 'to_boolean'
8
+ method :and, Object, cast: 'to_boolean'
9
+
10
+ # Equality
11
+ method :equals, String, as: '=='
12
+ method :differs, String, as: '!='
13
+ method :equals, Object, returns: false
14
+ method :differs, Object, returns: true
15
+
16
+ # Unary operators
17
+ method :not, cast: 'to_boolean'
18
+
19
+ # Size and length
20
+ method :size
21
+ method :length
22
+ method :empty, as: 'empty?'
23
+
24
+ # Concatenation
25
+ method :add, Fixnum, to: ->(fixnum) { self + fixnum.to_s }
26
+ method :add, String, as: '+'
27
+ method :append, String, as: '+'
28
+ method :prepend, String
29
+
30
+ # Repetition
31
+ method :multiply, Fixnum, to: ->(count) { Support::Utilities.repeat self, count }
32
+ method :repeat, Fixnum, to: ->(count) { Support::Utilities.repeat self, count }
33
+
34
+ # Truncation
35
+ method :truncate, Fixnum, to: ->(length) { Support::Utilities.truncate self, false, length, ' ...' }
36
+ method :truncate, Fixnum, String, to: ->(length, omission) { Support::Utilities.truncate self, false, length, omission }
37
+ method :truncate_words, Fixnum, to: ->(length) { Support::Utilities.truncate self, true, length, ' ...' }
38
+ method :truncate_words, Fixnum, String, to: ->(length, omission) { Support::Utilities.truncate self, true, length, omission }
39
+
40
+ # Replacement
41
+ method :replace, String, String, to: ->(replace, with) { self.gsub replace, with }
42
+ method :replace_first, String, String, to: ->(replace, with) { self.sub replace, with }
43
+
44
+ # Removal
45
+ method :subtract, String, to: ->(remove) { self.gsub remove, '' }
46
+ method :remove, String, to: ->(remove) { self.gsub remove, '' }
47
+ method :remove_first, String, to: ->(remove) { self.sub remove, '' }
48
+
49
+ # Case manipulation
50
+ method :capitalize
51
+ method :upcase
52
+ method :downcase
53
+
54
+ # Reversal
55
+ method :reverse
56
+
57
+ # Whitespace removal
58
+ method :strip
59
+ method :strip_left, as: 'lstrip'
60
+ method :strip_right, as: 'rstrip'
61
+ end
62
+ end
@@ -0,0 +1,49 @@
1
+ module Scribble
2
+ module Parsing
3
+ class Nester
4
+ def initialize nodes
5
+ @nodes = nodes
6
+ end
7
+
8
+ def nodes root = true
9
+ [].tap do |nodes|
10
+ while step(root)
11
+ nodes << node
12
+ if node.block?
13
+ @current = node
14
+ node.nodes = nodes false
15
+ end
16
+ end
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def step root
23
+ @cursor = @cursor ? @cursor + 1 : 0
24
+
25
+ if node.is_a? Nodes::Ending
26
+ raise unexpected_end if root
27
+ false
28
+ elsif node.nil?
29
+ raise unexpected_eot unless root
30
+ false
31
+ else
32
+ true
33
+ end
34
+ end
35
+
36
+ def node
37
+ @nodes[@cursor]
38
+ end
39
+
40
+ def unexpected_end
41
+ Errors::Syntax.new "Unexpected 'end' #{node.line_and_column}; no block currently open"
42
+ end
43
+
44
+ def unexpected_eot
45
+ Errors::Syntax.new "Unexpected end of template; unclosed '#{@current.name}' block #{@current.line_and_column}"
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,132 @@
1
+ require 'parslet'
2
+
3
+ module Scribble
4
+ module Parsing
5
+ class DelimitedChunk < Parslet::Atoms::Base
6
+ def initialize delimiters, min_chars = 0
7
+ @delimiters = delimiters
8
+ @min_chars = min_chars
9
+ end
10
+
11
+ def try(source, context, consume_all)
12
+ excluding_length = @delimiters.map {|d| source.chars_until(d) }.min
13
+
14
+ if excluding_length >= @min_chars
15
+ return succ(source.consume([excluding_length, 100000].min)) # max 100000 character atom, otherwise error in resulting regexp
16
+ else
17
+ return context.err(self, source, "No such string in input: #{@delimiters.inspect}.")
18
+ end
19
+ end
20
+
21
+ def to_s_inner(prec)
22
+ "until('#{@delimiters.inspect}')"
23
+ end
24
+ end
25
+
26
+ class Parser < Parslet::Parser
27
+ root(:template)
28
+
29
+ # Text, tags and endings
30
+ rule(:template) { (text | ending | tag).repeat.as(:template) }
31
+ rule(:text) { DelimitedChunk.new(['{{'], 1).as(:text) }
32
+ rule(:ending) { ltag >> str('end').as(:ending) >> rtag }
33
+ rule(:tag) { ltag >> (operation >> rtag | tag_command | rtag) }
34
+
35
+ # Command ending in rtag
36
+ rule(:tag_command) { tag_simple_command | (unary >> tag_command_tail).as(:chain) }
37
+ rule(:tag_command_tail) { dot >> (tag_simple_command.repeat(1,1) | simple_function >> tag_command_tail) }
38
+ rule(:tag_simple_command) { (name >> space >> tag_args).as(:call) }
39
+
40
+ # Command ending in rparen
41
+ rule(:paren_command) { paren_simple_command | (unary >> paren_command_tail).as(:chain) }
42
+ rule(:paren_command_tail) { dot >> (paren_simple_command.repeat(1,1) | simple_function >> paren_command_tail) }
43
+ rule(:paren_simple_command) { (name >> space >> paren_args).as(:call) }
44
+
45
+ # Entry point to operations
46
+ rule(:operation) { logical_or }
47
+
48
+ # Or / and
49
+ rule(:logical_or) { (logical_and >> ((pipe.as(:op) >> logical_and.as(:arg)).as(:or)).repeat(1)).as(:chain) | logical_and }
50
+ rule(:logical_and) { (equality >> ((ampersand.as(:op) >> equality.as(:arg)).as(:and)).repeat(1)).as(:chain) | equality }
51
+
52
+ # Equality / inequality
53
+ rule(:equality) { (comparison >> (equals | differs).repeat(1)).as(:chain) | comparison }
54
+ rule(:equals) { (equals_sign.as(:op) >> comparison.as(:arg)).as(:equals) }
55
+ rule(:differs) { (bang_equals.as(:op) >> comparison.as(:arg)).as(:differs) }
56
+
57
+ # Comparisons
58
+ rule(:comparison) { (additive >> (greater | less | greater_or_equal | less_or_equal).repeat(1)).as(:chain) | additive }
59
+ rule(:greater) { (rangle.as(:op) >> additive.as(:arg)).as(:greater) }
60
+ rule(:less) { (langle.as(:op) >> additive.as(:arg)).as(:less) }
61
+ rule(:greater_or_equal) { (rangle_equals.as(:op) >> additive.as(:arg)).as(:greater_or_equal) }
62
+ rule(:less_or_equal) { (langle_equals.as(:op) >> additive.as(:arg)).as(:less_or_equal) }
63
+
64
+ # Add / subtract
65
+ rule(:additive) { (multitive >> (add | subtract).repeat(1)).as(:chain) | multitive }
66
+ rule(:add) { (plus.as(:op) >> multitive.as(:arg)).as(:add) }
67
+ rule(:subtract) { (dash.as(:op) >> multitive.as(:arg)).as(:subtract) }
68
+
69
+ # Multiply / divide
70
+ rule(:multitive) { (function >> (multiply | divide | remainder).repeat(1)).as(:chain) | function }
71
+ rule(:multiply) { (asterisk.as(:op) >> function.as(:arg)).as(:multiply) }
72
+ rule(:divide) { (slash.as(:op) >> function.as(:arg)).as(:divide) }
73
+ rule(:remainder) { (percent.as(:op) >> function.as(:arg)).as(:remainder) }
74
+
75
+ # Functions
76
+ rule(:function) { (unary >> (dot >> simple_function).repeat(1)).as(:chain) | unary }
77
+ rule(:simple_function) { (name >> lparen >> (rparen | paren_args)).as(:call) | (name >> space?).as(:call_or_variable) }
78
+
79
+ # Unary operators
80
+ rule(:unary) { unary_operand | unary_negative | unary_not }
81
+ rule(:unary_negative) { (dash.as(:op) >> unary.as(:receiver)).as(:negative) }
82
+ rule(:unary_not) { (bang.as(:op) >> unary.as(:receiver)).as(:not) }
83
+
84
+ # Unary operand
85
+ rule(:unary_operand) { value >> space? | parentheses | simple_function }
86
+ rule(:parentheses) { lparen >> (operation >> rparen | paren_command) }
87
+
88
+ # Values
89
+ rule(:value) { str('true').as(:true) | str('false').as(:false) | empty_string | string | number }
90
+ rule(:string) { str("'") >> (str("\\") >> any | str("'").absent? >> any).repeat(1).as(:string) >> str("'") }
91
+ rule(:empty_string) { str("'") >> str('').as(:string) >> str("'") }
92
+ rule(:number) { match['0-9'].repeat(1).as(:number) }
93
+
94
+ # Name and arguments
95
+ rule(:name) { (match['a-z'] >> match['a-z0-9_'].repeat >> match['?!'].maybe).as(:name) }
96
+ rule(:paren_args) { (operation >> (comma >> operation).repeat >> rparen | paren_command).as(:args) }
97
+ rule(:tag_args) { (operation >> (comma >> operation).repeat >> rtag | tag_command).as(:args) }
98
+
99
+ # Spaces
100
+ rule(:space) { match('\s').repeat(1) }
101
+ rule(:space?) { space.maybe }
102
+
103
+ # Tag delimiters
104
+ rule(:ltag) { str('{{') >> space? }
105
+ rule(:rtag) { space? >> str('}}') }
106
+
107
+ # Parens
108
+ rule(:lparen) { str('(') >> space? }
109
+ rule(:rparen) { str(')') >> space? }
110
+
111
+ # Dot, comma
112
+ rule(:comma) { str(',') >> space? }
113
+ rule(:dot) { str('.') }
114
+
115
+ # Operators
116
+ rule(:pipe) { str('|') >> space? }
117
+ rule(:ampersand) { str('&') >> space? }
118
+ rule(:equals_sign) { str('=') >> space? }
119
+ rule(:bang_equals) { str('!=') >> space? }
120
+ rule(:langle) { str('<') >> space? }
121
+ rule(:rangle) { str('>') >> space? }
122
+ rule(:langle_equals) { str('<=') >> space? }
123
+ rule(:rangle_equals) { str('>=') >> space? }
124
+ rule(:plus) { str('+') >> space? }
125
+ rule(:dash) { str('-') >> space? }
126
+ rule(:asterisk) { str('*') >> space? }
127
+ rule(:slash) { str('/') >> space? }
128
+ rule(:percent) { str('%') >> space? }
129
+ rule(:bang) { str('!') >> space? }
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,71 @@
1
+ module Scribble
2
+ module Parsing
3
+ class Reporter
4
+ Cause = Struct.new :source, :position do
5
+ def strings
6
+ @strings ||= []
7
+ end
8
+
9
+ def raise
10
+ Kernel.raise Errors::Syntax.new "#{unexpected} at line #{line} column #{column}#{explanation}"
11
+ end
12
+
13
+ def unexpected
14
+ if character
15
+ "Unexpected '#{character}'"
16
+ else
17
+ "Unexpected end of template"
18
+ end
19
+ end
20
+
21
+ def character
22
+ source.instance_variable_get(:@str).string[position]
23
+ end
24
+
25
+ def line
26
+ source.line_and_column(position).first
27
+ end
28
+
29
+ def column
30
+ source.line_and_column(position).last
31
+ end
32
+
33
+ def explanation
34
+ if unterminated_string?
35
+ "; unterminated string"
36
+ else
37
+ ", expected #{Support::Utilities.to_sentence expected}"
38
+ end
39
+ end
40
+
41
+ def unterminated_string?
42
+ strings.all? { |string| ["\\", "'"].include? string }
43
+ end
44
+
45
+ def expected
46
+ strings.map do |string|
47
+ if string == 'true' then "a value"
48
+ elsif string == '+' then "an operator"
49
+ elsif string == '(' then "'('"
50
+ elsif string == '}}' then "'}}'"
51
+ elsif string == 'end' then "'end'"
52
+ end
53
+ end.compact.uniq.sort
54
+ end
55
+ end
56
+
57
+ def err atom, source, message, children = nil
58
+ err_at atom, source, message, source.pos, children
59
+ end
60
+
61
+ def err_at atom, source, message, position, children = nil
62
+ if @position.nil? || position > @position
63
+ @position = position
64
+ @cause = Cause.new source, position
65
+ end
66
+ @cause.strings << atom.str if position == @position && atom.respond_to?(:str)
67
+ @cause
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,87 @@
1
+ require 'parslet'
2
+
3
+ module Scribble
4
+ module Parsing
5
+ class Transform < Parslet::Transform
6
+
7
+ # Template, text and endings
8
+
9
+ rule template: sequence(:nodes) do
10
+ Nester.new nodes
11
+ end
12
+
13
+ rule text: simple(:slice) do
14
+ Nodes::Value.new slice, slice.to_s
15
+ end
16
+
17
+ rule ending: simple(:slice) do
18
+ Nodes::Ending.new slice
19
+ end
20
+
21
+ # Binary operators
22
+
23
+ %i(or and equals differs
24
+ greater less greater_or_equal less_or_equal
25
+ add subtract multiply divide remainder
26
+ ).each do |operator|
27
+ rule operator => {op: simple(:slice), arg: simple(:arg)} do
28
+ Nodes::Call.new slice, operator, args: [arg]
29
+ end
30
+ end
31
+
32
+ # Unary operators
33
+
34
+ %i(negative not).each do |operator|
35
+ rule operator => {op: simple(:slice), receiver: simple(:receiver)} do
36
+ Nodes::Call.new slice, operator, receiver: receiver
37
+ end
38
+ end
39
+
40
+ # Left associative operation reduction
41
+
42
+ rule chain: sequence(:calls) do
43
+ calls.reduce calls.shift do |base, call|
44
+ call.receiver = base; call
45
+ end
46
+ end
47
+
48
+ # Calls with no, one or multiple arguments
49
+
50
+ rule call: {name: simple(:slice)} do
51
+ Nodes::Call.new slice, slice.to_sym
52
+ end
53
+
54
+ rule call: {name: simple(:slice), args: simple(:arg)} do
55
+ Nodes::Call.new slice, slice.to_sym, args: [arg]
56
+ end
57
+
58
+ rule call: {name: simple(:slice), args: sequence(:args)} do
59
+ Nodes::Call.new slice, slice.to_sym, args: args
60
+ end
61
+
62
+ # Call without arguments or parentheses
63
+
64
+ rule call_or_variable: {name: simple(:slice)} do
65
+ Nodes::Call.new slice, slice.to_sym, allow_variable: true
66
+ end
67
+
68
+ # Values
69
+
70
+ rule number: simple(:slice) do
71
+ Nodes::Value.new slice, slice.to_i
72
+ end
73
+
74
+ rule string: simple(:slice) do
75
+ Nodes::Value.new slice, slice.to_s.gsub(/\\(.)/, '\1')
76
+ end
77
+
78
+ rule true: simple(:slice) do
79
+ Nodes::Value.new slice, true
80
+ end
81
+
82
+ rule false: simple(:slice) do
83
+ Nodes::Value.new slice, false
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,41 @@
1
+ module Scribble
2
+ class Partial
3
+ attr_reader :format
4
+
5
+ def initialize source, format: nil
6
+ @source, @format = source, format
7
+ end
8
+
9
+ # Parse and transform
10
+
11
+ def parse
12
+ @parse ||= Parsing::Parser.new.parse @source, reporter: Parsing::Reporter.new
13
+ end
14
+
15
+ def transform
16
+ @transform ||= Parsing::Transform.new.apply parse
17
+ end
18
+
19
+ def nodes
20
+ @nodes ||= transform.nodes
21
+ end
22
+
23
+ # Partial context
24
+
25
+ class Context
26
+ include Support::Context
27
+
28
+ def initialize partial, context
29
+ @partial, @context = partial, context
30
+ end
31
+
32
+ def nodes
33
+ @partial.nodes
34
+ end
35
+
36
+ def format
37
+ @partial.format || @context.format
38
+ end
39
+ end
40
+ end
41
+ end