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