scribble 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +22 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +455 -0
- data/Rakefile +2 -0
- data/lib/scribble.rb +44 -0
- data/lib/scribble/block.rb +25 -0
- data/lib/scribble/converter.rb +10 -0
- data/lib/scribble/errors.rb +24 -0
- data/lib/scribble/method.rb +91 -0
- data/lib/scribble/methods/if.rb +26 -0
- data/lib/scribble/methods/layout.rb +25 -0
- data/lib/scribble/methods/partial.rb +14 -0
- data/lib/scribble/methods/times.rb +11 -0
- data/lib/scribble/nodes/call.rb +55 -0
- data/lib/scribble/nodes/ending.rb +6 -0
- data/lib/scribble/nodes/node.rb +24 -0
- data/lib/scribble/nodes/value.rb +16 -0
- data/lib/scribble/objects/boolean.rb +33 -0
- data/lib/scribble/objects/fixnum.rb +53 -0
- data/lib/scribble/objects/nil.rb +21 -0
- data/lib/scribble/objects/string.rb +62 -0
- data/lib/scribble/parsing/nester.rb +49 -0
- data/lib/scribble/parsing/parser.rb +132 -0
- data/lib/scribble/parsing/reporter.rb +71 -0
- data/lib/scribble/parsing/transform.rb +87 -0
- data/lib/scribble/partial.rb +41 -0
- data/lib/scribble/registry.rb +95 -0
- data/lib/scribble/support/context.rb +98 -0
- data/lib/scribble/support/matcher.rb +74 -0
- data/lib/scribble/support/unmatched.rb +70 -0
- data/lib/scribble/support/utilities.rb +49 -0
- data/lib/scribble/template.rb +61 -0
- data/lib/scribble/version.rb +3 -0
- data/scribble.gemspec +22 -0
- data/test/all.rb +23 -0
- data/test/errors_test.rb +94 -0
- data/test/methods/if_test.rb +49 -0
- data/test/methods/layout_test.rb +71 -0
- data/test/methods/partial_test.rb +85 -0
- data/test/methods/times_test.rb +10 -0
- data/test/objects/boolean_test.rb +162 -0
- data/test/objects/fixnum_test.rb +236 -0
- data/test/objects/nil_test.rb +83 -0
- data/test/objects/string_test.rb +268 -0
- data/test/parsing/parsing_test.rb +234 -0
- data/test/registry_test.rb +264 -0
- data/test/template_test.rb +51 -0
- data/test/test_helper.rb +65 -0
- 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
|