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
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 06603b30016b49347e52550b6752f2615fa04841
|
4
|
+
data.tar.gz: 05e1e266eb511628463acf636a64c5c35001e094
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 682a2b60adffa14b19f74324984a04012da5ebf93bcf369e14a7eb19c488dc40ab73f634af71566c484092db8e686eb581cdd0127de6c3aa75d028510ba2c91d
|
7
|
+
data.tar.gz: 7b988a5dee13ac01f8de82ba87d53503d536857c5fcffb25bc149141763159d0dee497913a1bf54a81fcca14d216f2d92ff3def90d2f74b8a7985842987e03fc
|
data/.gitignore
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
.bundle
|
4
|
+
.config
|
5
|
+
.yardoc
|
6
|
+
Gemfile.lock
|
7
|
+
InstalledFiles
|
8
|
+
_yardoc
|
9
|
+
coverage
|
10
|
+
doc/
|
11
|
+
lib/bundler/man
|
12
|
+
pkg
|
13
|
+
rdoc
|
14
|
+
spec/reports
|
15
|
+
test/tmp
|
16
|
+
test/version_tmp
|
17
|
+
tmp
|
18
|
+
*.bundle
|
19
|
+
*.so
|
20
|
+
*.o
|
21
|
+
*.a
|
22
|
+
mkmf.log
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Stefan Kroes
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,455 @@
|
|
1
|
+
# Scribble
|
2
|
+
|
3
|
+
Scribble is a client facing template language similar to Liquid. Scribble was written in Ruby and can be used in any Ruby or Ruby on Rails project. It takes a template file, consisting of text and Scribble tags and transforms it into text. Scribble can be used to transform any plain text format like HTML, Markdown, JSON, XML or plain text. Client facing means that it is safe to use Scribble to run/evaluate user provided templates.
|
4
|
+
|
5
|
+
## Project status and features
|
6
|
+
|
7
|
+
Scribble currently has solid architecture and features and it is already used in production in [Sitebox.io](http://www.sitebox.io/). The actual scripting API is still somewhat sparse regarding supported types and methods but will be extended in future minor releases and can also easily be extended on a per application basis. Pull requests enriching the API are very welcome. Scribble currently supports:
|
8
|
+
|
9
|
+
* A proper grammar based parser, generating user-friendly syntax errors that unclude line and column numbers
|
10
|
+
* Excellent runtime error reporting (`Wrong number of arguments (0 for 1-2) for 'partial' at line 1 column 4`)
|
11
|
+
* Simple and consistent tag syntax using {{ ... }}
|
12
|
+
* Unary and binary operators with proper precedence rules and parentheses
|
13
|
+
* Method invocation, method chaining, block methods and command style method invocation (without parentheses)
|
14
|
+
* Nested execution scopes (blocks) and rescursive resolving of methods and variables
|
15
|
+
* Boolean, integer, string and nil scripting types (Arrays or hashes are not currently supported)
|
16
|
+
* Rich object oriented API for scripting with these types
|
17
|
+
* Pluggable architecture for additional (application specific) types
|
18
|
+
* If/elsif/else, layout and partial methods
|
19
|
+
* Pluggable loader architecture for working with partials and layouts
|
20
|
+
* Ability to convert between formats when inlining partials and layouts with mixed formats (for example: Markdown and HTML)
|
21
|
+
|
22
|
+
## Installation
|
23
|
+
|
24
|
+
Add this line to your application's Gemfile:
|
25
|
+
|
26
|
+
gem 'scribble'
|
27
|
+
|
28
|
+
And then execute:
|
29
|
+
|
30
|
+
$ bundle
|
31
|
+
|
32
|
+
Or install it yourself as:
|
33
|
+
|
34
|
+
$ gem install scribble
|
35
|
+
|
36
|
+
## Basic Usage
|
37
|
+
|
38
|
+
The following example shows how to load a Scribble template and evaluate it.
|
39
|
+
|
40
|
+
``` ruby
|
41
|
+
Scribble::Template.new("I'm a template! {{1 + 1}}").render
|
42
|
+
```
|
43
|
+
|
44
|
+
The template constructor and and render method both take options. The following example shows the same call but with all options. We will describe the specifics of formats, loaders, converters and registers later.
|
45
|
+
|
46
|
+
``` ruby
|
47
|
+
template_options = {
|
48
|
+
format: :markdown, # The template itself is in Markdown format
|
49
|
+
loader: my_partial_loader, # Object handling the loading of partials and layouts
|
50
|
+
converters: [html2md, md2html] # Two converters to convert between HTML and Markdown
|
51
|
+
}
|
52
|
+
|
53
|
+
render_options = {
|
54
|
+
variables: {a: 1, b: 'Foo'}, # Expose some data to the template
|
55
|
+
registers: {c: 2, d: 'Bar'}, # For internal use by Scribble method implementations
|
56
|
+
format: :html # Request output conversion to HTML format
|
57
|
+
}
|
58
|
+
|
59
|
+
Scribble::Template.new(source, template_options).render render_options
|
60
|
+
```
|
61
|
+
|
62
|
+
## Basic Template
|
63
|
+
|
64
|
+
The following example is a Scribble template repeating some text, folowed by an `if` statement.
|
65
|
+
|
66
|
+
{{ 3.times }}Hello World!{{ end }}
|
67
|
+
|
68
|
+
{{ if foo = 3 | bar = 'baz' }}
|
69
|
+
Either foo equals the number 3 or bar equals the string 'baz'
|
70
|
+
{{ elsif 3 * 3 != foo & bar }}
|
71
|
+
Foo does not equal 9 and bar is either true, a non-zero number or a non-empty string
|
72
|
+
{{ else }}
|
73
|
+
None of the above
|
74
|
+
{{ end }}
|
75
|
+
|
76
|
+
In the example above:
|
77
|
+
|
78
|
+
* Everything between {{ and }} are Scribble tags, the rest is text.
|
79
|
+
* `times` and `if` are block methods that can be used to manipulate the associated block (up until the next `end`).
|
80
|
+
* `foo` and `bar` are variables that can be inserted by passing them into the `render` method from Ruby.
|
81
|
+
* `=`, `|`, `&`, `*`, `!=` are operators. Operators are invoked on their left hand side with their right hand side as an argument.
|
82
|
+
* `elsif` and `else` are methods that are defined only in the context of an `if` block to split up the block.
|
83
|
+
|
84
|
+
## Template and rendering options
|
85
|
+
|
86
|
+
Scribble supports a few options when initializing and rendering templates. This section describes those options.
|
87
|
+
|
88
|
+
### Supplying a partial loader
|
89
|
+
|
90
|
+
When you supply Scribble with a partial loader you can use the `partial` and `layout` methods in order to inline other Scribble templates. A loader is an objects that implements a single instance method named `load` which takes the partial name as a single string argument. The method should return a Scribble::Partial or nil if the partial cannot be found. Initializing the partial with a format is optional.
|
91
|
+
|
92
|
+
``` ruby
|
93
|
+
class MyLoader
|
94
|
+
def load name
|
95
|
+
|
96
|
+
# ...
|
97
|
+
|
98
|
+
unless partial_source.nil?
|
99
|
+
Scribble::Partial.new partial_source, format: partial_format
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
Scribble::Template.new(template_source, loader: MyLoader.new).render
|
105
|
+
```
|
106
|
+
|
107
|
+
### Formats and converters
|
108
|
+
|
109
|
+
Scribble is able to convert between different formats when inlining partials and rendering templates. This functionality is completely optional, just ignore all format and converter options to disable it. If you want to use this functionality you should tell Scribble the format of your templates and partials when initializing them using symbols. Additionally you should tell Scribble in what format to render and supply the format convertors that will be needed to do so.
|
110
|
+
|
111
|
+
``` ruby
|
112
|
+
md2html = Scribble.converter :markdown => :html do |markdown|
|
113
|
+
Kramdown::Document.new(markdown).to_html
|
114
|
+
end
|
115
|
+
|
116
|
+
template = Scribble::Template.new template_source, format: :markdown, converters: [md2html]
|
117
|
+
|
118
|
+
template.render :html
|
119
|
+
```
|
120
|
+
|
121
|
+
### Variables and registers
|
122
|
+
|
123
|
+
Variables are the way to pass data from your template into your Scribble template. In addition to variables, you can pas registers. Registers will not be available as Scribble variables but they will be available to the Ruby implementations of your methods.
|
124
|
+
|
125
|
+
|
126
|
+
``` ruby
|
127
|
+
template = Scribble::Template.new template_source
|
128
|
+
|
129
|
+
template.render variables: {a: 1}, registers: {some_private_resource: 2}
|
130
|
+
```
|
131
|
+
|
132
|
+
## Template Language
|
133
|
+
|
134
|
+
The Scribble template language takes a lot of cues from Ruby:
|
135
|
+
|
136
|
+
* Everything is an object
|
137
|
+
* Objects only support methods, no attributes
|
138
|
+
* Methods can take a block of code, delimited by the keyword `end`
|
139
|
+
* Operators are just method calls, invoked on their left hand side
|
140
|
+
* Parentheses are optional when passing arguments into a method
|
141
|
+
* Multiple methods can be chained using dot `.` notation
|
142
|
+
|
143
|
+
Some key differences are:
|
144
|
+
|
145
|
+
* Scribble only implements a very small subset of the Ruby syntax.
|
146
|
+
* Blocks of code don't need to be opened. The interpreter knows which methods take a block.
|
147
|
+
* Constructs like an `if` statement are also implemented as block methods, using so called split methods for constructs like `elsif` and `else`.
|
148
|
+
* Meanings of operators have been simplified because Scribble only needs to support certain operations.
|
149
|
+
|
150
|
+
### Syntax
|
151
|
+
|
152
|
+
The Scribble grammar is defined using [Parslet](http://kschiess.github.io/parslet/) in [parser.rb](https://github.com/stefankroes/scribble/blob/master/lib/scribble/parsing/parser.rb).
|
153
|
+
|
154
|
+
A Scribble template consists of text and tags. Tags are everything between {{ and }}. Tags containing only the keyword `end` are special tags that close a block.
|
155
|
+
|
156
|
+
3 times 6 is: {{ 3 * 6 }}
|
157
|
+
|
158
|
+
{{ 3.times }}
|
159
|
+
Hello World!
|
160
|
+
{{ end }}
|
161
|
+
|
162
|
+
Scribble supports integers, strings, booleans and nil. Nil doesn't have a literal, it can only be returned. Other objects do have literals.
|
163
|
+
|
164
|
+
{{ 'This is string' }}
|
165
|
+
|
166
|
+
This is a number: {{ 3 }}
|
167
|
+
|
168
|
+
{{ true }} and {{ false }} are booleans
|
169
|
+
|
170
|
+
Methods are called globally or on objects with the dot `.` notation. Parentheses are optional when invoking a method and are not allowed when calling a variable. Methods can be chained and used as arguments to other methods.
|
171
|
+
|
172
|
+
{{ some_method }}
|
173
|
+
{{ some_method() }}
|
174
|
+
{{ some_method(1, true, 'test') }}
|
175
|
+
{{ some_method 1, true, 'test' }}
|
176
|
+
{{ some_variable }}
|
177
|
+
{{ -3.abs }}
|
178
|
+
{{ 'Scribble'.replace('Scr', 'Dr').upcase }}
|
179
|
+
{{ true.or 1 }}
|
180
|
+
{{ 3.multiply(5.subtract(2)) }}
|
181
|
+
{{ some_method another_method }}
|
182
|
+
This will cause an error: {{ some_variable() }}
|
183
|
+
|
184
|
+
Unary and binary operators are supported. As are parentheses.
|
185
|
+
|
186
|
+
{{ -2 * (3 + 5) }}
|
187
|
+
{{ 'Scribble' - 'Scr' }}
|
188
|
+
{{ 3 * 'Repeat me! ' }}
|
189
|
+
|
190
|
+
### Operators and precedence
|
191
|
+
|
192
|
+
The table below shows all supported operators in order of precedence. Operators are invoked as methods on their left hand side (or on their only operand in case of a unary operator). The table also shows the method name that is associated with each operator for this purpose. This essentially enables operator overloading since you just define the associated method.
|
193
|
+
|
194
|
+
Operator | Precedence | Description | Associated method
|
195
|
+
------------------- | ---------- | ---------------- | -----------------
|
196
|
+
`!` | 1 | Unary *not* | `not`
|
197
|
+
`-` | 2 | Unary minus | `negative`
|
198
|
+
`*` | 3 | Multiplication | `multiply`
|
199
|
+
`/` | 3 | Division | `divide`
|
200
|
+
`%` | 3 | Remainder | `remainder`
|
201
|
+
`+` | 4 | Addition | `add`
|
202
|
+
`-` | 4 | Subtraction | `subtract`
|
203
|
+
`>` | 5 | Greater | `greater`
|
204
|
+
`<` | 5 | Less | `less`
|
205
|
+
`>=` | 5 | Greater or equal | `greater_or_equal`
|
206
|
+
`<=` | 5 | Less or equal | `less_or_equal`
|
207
|
+
`=` | 6 | Equality | `equals`
|
208
|
+
`!=` | 6 | Inequality | `differs`
|
209
|
+
`&` | 7 | Logical *and* | `and`
|
210
|
+
<code>|</code> | 8 | Logical *or* | `or`
|
211
|
+
|
212
|
+
## Standard types and methods
|
213
|
+
|
214
|
+
Scribble provides an extensible set of types and methods as a base API. This section describes Scribble functionality that is always available.
|
215
|
+
|
216
|
+
### String
|
217
|
+
|
218
|
+
Strings support a literal notation using single quotes. Single quotes and backslashes can be escaped within a string using a backslash (`\'` and `\\`). Strings are considered false when they are empty and true when they are not. Strings support many methods and many operators (through methods), an overview can be found in [objects/string.rb](https://github.com/stefankroes/scribble/blob/master/lib/scribble/objects/string.rb).
|
219
|
+
|
220
|
+
{{ 'I\'m a string!' }}
|
221
|
+
|
222
|
+
{{ if foo }}
|
223
|
+
Text in the string foo: {{ foo }}
|
224
|
+
{{ else }}
|
225
|
+
The string foo is empty!
|
226
|
+
{{ end }}
|
227
|
+
|
228
|
+
{{ 'foo' * 3 }}
|
229
|
+
{{ 'foo'.repeat 3 }}
|
230
|
+
{{ 'foo' + 'bar' }}
|
231
|
+
{{ 'foo' - 'oo' }}
|
232
|
+
{{ 'foo'.replace 'foo', 'bar' }}
|
233
|
+
{{ 'foo'.upcase.downcase.capitalize }}
|
234
|
+
{{ 'Hello world!'.truncate 5, '...' }}
|
235
|
+
{{ 'Hello world!'.truncate_words 1 }}
|
236
|
+
|
237
|
+
### Integer
|
238
|
+
|
239
|
+
Integers support a literal notation using one or more digits. Integers are considered false when they are zero and true when they are not. Integers support many methods and many operators (through methods), an overview can be found in [objects/fixnum.rb](https://github.com/stefankroes/scribble/blob/master/lib/scribble/objects/fixnum.rb). Additionally, integers support the times method for repeating a block of content multiple times. Times needs to be implemented as its own class because it takes a block.
|
240
|
+
|
241
|
+
{{ if foo }}
|
242
|
+
Integer foo: {{ foo }}
|
243
|
+
{{ else }}
|
244
|
+
The integer foo is zero!
|
245
|
+
{{ end }}
|
246
|
+
|
247
|
+
{{ (-5 + 3) * 5 / 3 }}
|
248
|
+
{{ 5.negative }}
|
249
|
+
{{ -3.abs }}
|
250
|
+
{{ 4.even }}
|
251
|
+
{{ 4.odd }}
|
252
|
+
{{ 4.times }}Hi! {{ end }}
|
253
|
+
|
254
|
+
### Boolean
|
255
|
+
|
256
|
+
Booleans support a literal notation using either the keyword true or the keyword false. Booleans support a subset of operators and associated methods, an overview can be found in [objects/boolean.rb](https://github.com/stefankroes/scribble/blob/master/lib/scribble/objects/boolean.rb).
|
257
|
+
|
258
|
+
{{ true | false & 'foo' }}
|
259
|
+
{{ !true }}
|
260
|
+
{{ true != 3 }}
|
261
|
+
|
262
|
+
### Nil
|
263
|
+
|
264
|
+
Nil does not support a literal notation but it can be returned by methods. It is always considered to be false. It implements the same methods as false, an overview can be found in [objects/nil.rb](https://github.com/stefankroes/scribble/blob/master/lib/scribble/objects/nil.rb).
|
265
|
+
|
266
|
+
### Global methods
|
267
|
+
|
268
|
+
Scribble supports the global `if`, `partial`, and `layout` methods. The if method was shown in several previous examples. The `partial` and `layout` methods are similar in that they load a partial through te loader and render it (converting between any format disparities). The difference is that layout takes a block of content to which the partial can yield using the `content` method.
|
269
|
+
|
270
|
+
# Partial template (loaded by loader as 'foo')
|
271
|
+
|
272
|
+
Hello from the partial!
|
273
|
+
|
274
|
+
# Layout template (loaded by loader as 'bar')
|
275
|
+
|
276
|
+
<div style="color=red;">{{ content }}</div>
|
277
|
+
|
278
|
+
# Template
|
279
|
+
|
280
|
+
{{ partial 'foo' }}
|
281
|
+
|
282
|
+
{{ layout 'foo' }}I will be in red!{{ end }}
|
283
|
+
|
284
|
+
## Extending the Scribble language
|
285
|
+
|
286
|
+
The Scribble language can be extended by inserting new methods into the registry. The registry keeps track of methods and their properties:
|
287
|
+
|
288
|
+
* The method name
|
289
|
+
* The receiver class/type
|
290
|
+
* The argument signature (number of arguments and their types)
|
291
|
+
|
292
|
+
Any Ruby class can also be a Scribble type as long as methods are defined on it.
|
293
|
+
|
294
|
+
### Introducing a new type
|
295
|
+
|
296
|
+
In order to add everything to the registry that will make a Ruby class well behaved as a Scribble type, use Scribble::Registry.for and define its name, cast methods and regular methods as shown below. After that you can just pass the Ruby object along to your template using variables.
|
297
|
+
|
298
|
+
``` ruby
|
299
|
+
Scribble::Registry.for User do
|
300
|
+
name 'user'
|
301
|
+
|
302
|
+
to_boolean { true }
|
303
|
+
to_string { "user: #{name}" }
|
304
|
+
|
305
|
+
# Logical operators
|
306
|
+
method :or, Object, cast: 'to_boolean'
|
307
|
+
method :and, Object, cast: 'to_boolean'
|
308
|
+
|
309
|
+
# Equality
|
310
|
+
method :equals, User, as: '=='
|
311
|
+
method :differs, User, as: '!='
|
312
|
+
method :equals, Object, returns: false
|
313
|
+
method :differs, Object, returns: true
|
314
|
+
|
315
|
+
# Attributes
|
316
|
+
method :name
|
317
|
+
method :email
|
318
|
+
|
319
|
+
method :first_name, to: -> { self.name.split(/\s/).first }
|
320
|
+
end
|
321
|
+
```
|
322
|
+
|
323
|
+
In the example above the call to `name` describes how the class `User` should be called in error messages and such. The next two lines describe how it should be cast to either a boolean (when used in an `if` statement for example) or a string (when it is rendered to the template). Then a bunch of methods are defined using different keywords.
|
324
|
+
|
325
|
+
* The `cast` keyword tells Scribble to cast this `User` to a boolean first, then call the same method (`or` or `and`) on that boolean.
|
326
|
+
* The `as` keyword means the method is delegated to the Ruby implementation under a different name.
|
327
|
+
* The `returns` keyword specifies that the method should just always return the same value.
|
328
|
+
* The `to` keyword specifies a block to implement the method.
|
329
|
+
* When `method` is called without any keywords, it is delegated to the Ruby method with the same name.
|
330
|
+
|
331
|
+
In this case `equals` and `differs` are defined twice with a different signature. The first method with a matching signature will be called so you can use pattern matching when implementing methods. In this case `User` can be equal to another user (delegated to Ruby equality) but can never be equal to another object (always returns false).
|
332
|
+
|
333
|
+
### Introducing a complex method
|
334
|
+
|
335
|
+
Global methods and any methods taking a block should be implemented as a Ruby class that subclasses either Scribble::Method or Scribble::Block. The `if` method is shown below as an example. Some more examples can be found in the [methods folder](https://github.com/stefankroes/scribble/tree/master/lib/scribble/methods) of the project.
|
336
|
+
|
337
|
+
``` ruby
|
338
|
+
class If < Scribble::Block
|
339
|
+
register :if, Object
|
340
|
+
|
341
|
+
def if object
|
342
|
+
@paths = []
|
343
|
+
send :elsif, object
|
344
|
+
|
345
|
+
render(nodes: @paths.map { |condition, nodes| nodes if condition }.compact.first || [])
|
346
|
+
end
|
347
|
+
|
348
|
+
method :elsif, Object, split: true
|
349
|
+
|
350
|
+
def elsif object
|
351
|
+
@paths.unshift [Registry.to_boolean(object), split_nodes]
|
352
|
+
end
|
353
|
+
|
354
|
+
method :else, split: true
|
355
|
+
|
356
|
+
def else
|
357
|
+
@paths.unshift [true, nodes]
|
358
|
+
end
|
359
|
+
end
|
360
|
+
```
|
361
|
+
|
362
|
+
The first line of the class implementation registers this method with the registry. The two calls to the `method` method register methods (`elsif` and `else`) that will be available within the block passed to the `if` call. These are split methods, meaning they will split up the block when `split_nodes` is called. Lets examine what happens when the following code is evaluated.
|
363
|
+
|
364
|
+
{{ if false }}
|
365
|
+
Some text
|
366
|
+
{{ elsif true }}
|
367
|
+
Some more text
|
368
|
+
{{ else }}
|
369
|
+
Last bit of text
|
370
|
+
{{ end }}
|
371
|
+
|
372
|
+
1. `if` is a block method so the block up to the `end` is assigned to it and the `elsif` and `else` methods are considered regular method calls by the parser.
|
373
|
+
2. `if` is called in Scribble which is delegated to the `if` method in Ruby
|
374
|
+
3. The `if` method initializes the path array and calls `elsif` (using `send` because `elsif` is a keyword)
|
375
|
+
4. `split_nodes` is called by `elsif` which skips over `Some text` and evaluates the Scribble `elsif` (the next split method)
|
376
|
+
5. `split_nodes` is called by `elsif` which skips over `Some more text` and evaluates the Scribble `else` (the next split method)
|
377
|
+
6. The implementation of `else` takes the rest of the nodes (`Last bit of text`) and inserts it into the array together with a `true` value.
|
378
|
+
7. The last call to `split_nodes` returns `Some more text` which is added to the beginning of the array, together with the argument to `elsif` cast to a boolean (`true`).
|
379
|
+
8. The first call to `split_nodes` returns `Some text` which is added to the beginning of the array, together with the argument to `if` cast to a boolean (`false`).
|
380
|
+
9. The `send :elsif` returns and it renders the first list of nodes with a positive condition (`Some more text`)
|
381
|
+
|
382
|
+
This might be hard to wrap your head around but once you do it is a clean and easy way to extend the language with new `if`-like constructs without touching the parser. For examples of this, check out [Sitebox.io forms](http://www.sitebox.io/articles/form-method) and [Sitebox.io columns](http://www.sitebox.io/articles/columns-method). Implementations of block methods that don't use split methods are much easier, lake a look at [the implementation](https://github.com/stefankroes/scribble/blob/master/lib/scribble/methods/times.rb) if the `times` method.
|
383
|
+
|
384
|
+
### Method implementation API
|
385
|
+
|
386
|
+
This sections describes the API you can use when implementing methods as classes (like the `if` method). Please read the previous section first to gain some context.
|
387
|
+
|
388
|
+
Method or instance variable | Description
|
389
|
+
------------------------------------- | ---------------------------------------
|
390
|
+
`@receiver` | The object receiving the method call (object before the method invocation dot or left hand side of an operation)
|
391
|
+
`@call` | The parse tree node of the method call
|
392
|
+
`@context` | The execution context (block) of the method call
|
393
|
+
`@context.variables` | The variables that are defined in that context (doesn't include variables of containing contexts)
|
394
|
+
`@context.set_variable name, value` | Set a variable in the context (and nested contexts)
|
395
|
+
`@context.registers` | The registers passed to the render method
|
396
|
+
`@context.template` | The template or partial being rendered
|
397
|
+
`@context.format` | The format of the text nodes in the context (before conversion)
|
398
|
+
`@context.render_format` | The target rendering format (after conversion)
|
399
|
+
`render` | (`Scribble::Block` only) Render the contents of the block (or specific nodes using the `nodes` keyword) to a string
|
400
|
+
`split_nodes` | (`Scribble::Block` only) Evaluate the next split method and return nodes that came before it
|
401
|
+
`nodes` | (`Scribble::Block` only) Get all nodes not skipped over by calling `split_nodes`
|
402
|
+
|
403
|
+
All methods available on `@context` are also available directly when the method is a `Scribble::Block` as it is also a context. `@context` holds the context containing the block in case of a `Scribble::Block`.
|
404
|
+
|
405
|
+
### Method signatures
|
406
|
+
|
407
|
+
In previous sections you have seen some method signatures. Both the `method` and `register` methods seen above take zero or more arguments after the method name. These arguments are the method signature. Any options/named arguments passed (`cast`, `as`, etc.) are not part of the method signature. A method matches when arguments of the same classes (or subclasses) as those in the signature are passed. Signatures also support optional arguments and multiple arguments as explained below.
|
408
|
+
|
409
|
+
``` ruby
|
410
|
+
Scribble::Registry.for String do
|
411
|
+
# method1 takes an integer and a required string
|
412
|
+
method :method1, Fixnum, String
|
413
|
+
|
414
|
+
# method2 takes an integer and zero or more strings
|
415
|
+
method :method2, Fixnum, [String]
|
416
|
+
|
417
|
+
# method3 takes an integer and up to one string (an optional string)
|
418
|
+
method :method3, Fixnum, [String, 1]
|
419
|
+
|
420
|
+
# method4 takes an integer, one to four strings and after that zero or more integers
|
421
|
+
method :method4, Fixnum, String, [String, 3], [Fixnum]
|
422
|
+
end
|
423
|
+
```
|
424
|
+
|
425
|
+
## Why Scribble instead of Liquid?
|
426
|
+
|
427
|
+
While Liquid is fine for many use-cases, we decided to replace it on the [Sitebox.io](http://www.sitebox.io/) project for the following reasons:
|
428
|
+
|
429
|
+
* Liquid uses regular expressions for parsing and tends to ignore most runtime errors, we want to be a little more strict and present the user with helpful error messages that include line and column numbers
|
430
|
+
* We needed a template language that's able to convert between different formats when using partials (in particular between Markdown and HTML)
|
431
|
+
* While the Liquid syntax can be friendly to non-programmers, an object oriented expression based language is more powerful and expressive
|
432
|
+
* Liquid uses different syntax for blocks and inlining, we wanted a single syntax for both
|
433
|
+
|
434
|
+
## Future work
|
435
|
+
|
436
|
+
* Extend language with arrays and possibly hashes
|
437
|
+
* Extend language with more built-in methods like loops
|
438
|
+
* Setup continuous integration and coverage reports
|
439
|
+
* Write more documentation, in particular on error reporting
|
440
|
+
|
441
|
+
## Contributing
|
442
|
+
|
443
|
+
1. Fork it ( https://github.com/stefankroes/scribble/fork )
|
444
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
445
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
446
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
447
|
+
5. Create a new Pull Request
|
448
|
+
|
449
|
+
## Origin, License, Copyright
|
450
|
+
|
451
|
+
Released under the [MIT license](https://github.com/stefankroes/scribble/blob/master/LICENSE.txt)
|
452
|
+
|
453
|
+
Scribble was developed by Stefan Kroes at [Lab 01](http://www.lab01.nl/) (Dutch website, available for hire ;-) as a part of [Sitebox.io](http://www.sitebox.io/) (Service that lets you create/edit a website using files in your Dropbox).
|
454
|
+
|
455
|
+
Copyright © 2014 Stefan Kroes
|