ember 0.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +15 -0
- data/bin/ember +47 -0
- data/doc/api/classes/Ember.html +61 -0
- data/doc/api/classes/Ember/Template.html +396 -0
- data/doc/api/classes/Ember/Template/Program.html +497 -0
- data/doc/api/created.rid +1 -0
- data/doc/api/css/main.css +263 -0
- data/doc/api/css/panel.css +383 -0
- data/doc/api/css/reset.css +53 -0
- data/doc/api/files/LICENSE.html +76 -0
- data/doc/api/files/lib/ember/template_rb.html +66 -0
- data/doc/api/files/lib/ember_rb.html +63 -0
- data/doc/api/i/arrows.png +0 -0
- data/doc/api/i/results_bg.png +0 -0
- data/doc/api/i/tree_bg.png +0 -0
- data/doc/api/index.html +14 -0
- data/doc/api/js/jquery-1.3.2.min.js +19 -0
- data/doc/api/js/jquery-effect.js +593 -0
- data/doc/api/js/main.js +22 -0
- data/doc/api/js/searchdoc.js +605 -0
- data/doc/api/panel/index.html +63 -0
- data/doc/api/panel/search_index.js +1 -0
- data/doc/api/panel/tree.js +1 -0
- data/doc/example.erb +2 -0
- data/doc/example.txt +2 -0
- data/doc/history.erb +9 -0
- data/doc/index.erb +11 -0
- data/doc/index.xhtml +758 -0
- data/doc/intro.erb +79 -0
- data/doc/setup.erb +29 -0
- data/doc/usage.erb +249 -0
- data/lib/ember.rb +16 -0
- data/lib/ember/template.rb +586 -0
- data/rakefile +14 -0
- data/test/ember.rb +17 -0
- data/test/ember/template.rb +141 -0
- metadata +109 -0
data/doc/intro.erb
ADDED
@@ -0,0 +1,79 @@
|
|
1
|
+
%#--
|
2
|
+
%# Copyright 2009 Suraj N. Kurapati
|
3
|
+
%# See the LICENSE file for details.
|
4
|
+
%#++
|
5
|
+
|
6
|
+
% api_url = './api/index.html'
|
7
|
+
% repo_url = 'http://github.com/sunaku/' + $program
|
8
|
+
% repo_scm = '[Git](http://git-scm.com)'
|
9
|
+
|
10
|
+
%|chapter "Introduction"
|
11
|
+
%|project
|
12
|
+
<%= $project %> is an [eRuby template](http://en.wikipedia.org/wiki/ERuby) processsor that facilitates debugging, reduces markup, and improves composability of eRuby templates.
|
13
|
+
|
14
|
+
<%= $project %> is exciting because:
|
15
|
+
* It reports correct line numbers in stack traces.
|
16
|
+
* It can infer <tt><%% end %></tt> based on indentation.
|
17
|
+
* It can unindent block content hierarchically.
|
18
|
+
* It completely silences code-only eRuby directives.
|
19
|
+
* It is implemented in <%= `sloccount lib`[/^\d+/] %> lines of pure Ruby.
|
20
|
+
|
21
|
+
These features distinguish <%= $project %> from the competition:
|
22
|
+
* [Erubis](http://www.kuwata-lab.com/erubis/)
|
23
|
+
* [eruby](http://modruby.net/en/)
|
24
|
+
* [ERB](http://www.ruby-doc.org/stdlib/libdoc/erb/rdoc/)
|
25
|
+
|
26
|
+
%|paragraph "Etymology"
|
27
|
+
<%= $project %> stands for *embe*dded *r*uby.
|
28
|
+
|
29
|
+
%|section "Logistics"
|
30
|
+
* <%= xref "History", "Release notes" %> --- history of project releases.
|
31
|
+
* [Source code](<%= repo_url %>) --- obtain via <%= repo_scm %> or browse online.
|
32
|
+
* [API reference](<%= api_url %>) --- documentation for source code.
|
33
|
+
* [Project home](<%= $website %>) --- the <%= $project %> project home page.
|
34
|
+
|
35
|
+
To get help or provide feedback, simply
|
36
|
+
<%= xref "License", "contact the author(s)" %>.
|
37
|
+
|
38
|
+
%|paragraph "Version numbers"
|
39
|
+
<%= $project %> releases are numbered in *major.minor.patch*
|
40
|
+
form according to the [RubyGems rational versioning
|
41
|
+
policy](http://www.rubygems.org/read/chapter/7), which
|
42
|
+
can be summarized thus:
|
43
|
+
|
44
|
+
<table markdown="1">
|
45
|
+
<thead>
|
46
|
+
<tr>
|
47
|
+
<td rowspan="2">What increased in the version number?</td>
|
48
|
+
<td colspan="3">The increase indicates that the release:</td>
|
49
|
+
</tr>
|
50
|
+
<tr>
|
51
|
+
<th>Is backward compatible?</th>
|
52
|
+
<th>Has new features?</th>
|
53
|
+
<th>Has bug fixes?</th>
|
54
|
+
</tr>
|
55
|
+
</thead>
|
56
|
+
<tbody>
|
57
|
+
<tr>
|
58
|
+
<th>major</th>
|
59
|
+
<td style="background-color: #FFE4E1;">No</td>
|
60
|
+
<td>Yes</td>
|
61
|
+
<td>Yes</td>
|
62
|
+
</tr>
|
63
|
+
<tr>
|
64
|
+
<th>minor</th>
|
65
|
+
<td>Yes</td>
|
66
|
+
<td>Yes</td>
|
67
|
+
<td>Yes</td>
|
68
|
+
</tr>
|
69
|
+
<tr>
|
70
|
+
<th>patch</th>
|
71
|
+
<td>Yes</td>
|
72
|
+
<td style="background-color: #FFE4E1;">No</td>
|
73
|
+
<td>Yes</td>
|
74
|
+
</tr>
|
75
|
+
</tbody>
|
76
|
+
</table>
|
77
|
+
|
78
|
+
%|paragraph "License"
|
79
|
+
%< "../LICENSE"
|
data/doc/setup.erb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
%#--
|
2
|
+
%# Copyright 2009 Suraj N. Kurapati
|
3
|
+
%# See the LICENSE file for details.
|
4
|
+
%#++
|
5
|
+
|
6
|
+
%|chapter "Setup"
|
7
|
+
%|section "Requirements"
|
8
|
+
Your system needs the following software to run <%= $project %>.
|
9
|
+
|
10
|
+
| Software | Description | Notes |
|
11
|
+
| -------- | ----------- | ----- |
|
12
|
+
| [Ruby](http://ruby-lang.org) | Ruby language interpreter | Version 1.8.6, 1.8.7, and 1.9.1 have been tested successfully. |
|
13
|
+
| [RubyGems](http://rubygems.org) | Ruby packaging system | Version 1.3.1 or newer is required. |
|
14
|
+
|
15
|
+
%|section "Installation"
|
16
|
+
You can install <%= $project %> by running this command:
|
17
|
+
|
18
|
+
gem install <%= $program %>
|
19
|
+
|
20
|
+
To check whether the installation was sucessful, run this command:
|
21
|
+
|
22
|
+
<%= $program %> --version
|
23
|
+
|
24
|
+
If the installation was successful, you will see output like this:
|
25
|
+
|
26
|
+
<pre><%= verbatim `ruby bin/#{$program} --version` %></pre>
|
27
|
+
|
28
|
+
If you do not see such output, you may
|
29
|
+
<%= xref "License", "ask the author(s)" %> for help.
|
data/doc/usage.erb
ADDED
@@ -0,0 +1,249 @@
|
|
1
|
+
%#--
|
2
|
+
%# Copyright 2009 Suraj N. Kurapati
|
3
|
+
%# See the LICENSE file for details.
|
4
|
+
%#++
|
5
|
+
|
6
|
+
%|chapter "Usage"
|
7
|
+
%|section "Command-line interface"
|
8
|
+
When you run this command:
|
9
|
+
|
10
|
+
<%= $program %> --help
|
11
|
+
|
12
|
+
You will see this output:
|
13
|
+
|
14
|
+
<pre><%= verbatim `ruby bin/#{$program} --help` %></pre>
|
15
|
+
|
16
|
+
%|section "Ruby library interface"
|
17
|
+
Begin by loading <%= $project %> into Ruby:
|
18
|
+
|
19
|
+
<code>
|
20
|
+
require 'rubygems'
|
21
|
+
require '<%= $program %>'
|
22
|
+
</code>
|
23
|
+
|
24
|
+
Instantiate a template processor:
|
25
|
+
|
26
|
+
<code>
|
27
|
+
source = "your eRuby template here"
|
28
|
+
options = { :unindent => true, :shorthand => true }
|
29
|
+
|
30
|
+
template = Ember::Template.new(source, options)
|
31
|
+
</code>
|
32
|
+
|
33
|
+
Inspect the Ruby program that is used to evaluate the eRuby template:
|
34
|
+
|
35
|
+
<code>
|
36
|
+
puts template.program
|
37
|
+
</code>
|
38
|
+
|
39
|
+
View the result of evaluating the eRuby template:
|
40
|
+
|
41
|
+
<code>
|
42
|
+
puts template.render
|
43
|
+
</code>
|
44
|
+
|
45
|
+
See the [API documentation](<%= api_url %>) for details and examples.
|
46
|
+
|
47
|
+
%|section "eRuby template directives", "Directives"
|
48
|
+
eRuby templates are plain-text documents that contain special processing instructions known as **directives**. Directives may be expressed using either **standard** or **shorthand** notation:
|
49
|
+
|
50
|
+
| Notation | Directive | Head | Operation | Body | Tail |
|
51
|
+
| -------- | --------- | ---- | --------- | ---- | ---- |
|
52
|
+
| Standard | <%%XY%> | <%% | X | Y | %> |
|
53
|
+
| Shorthand | %XY | % | X | Y | |
|
54
|
+
|
55
|
+
In standard notation, the directive is composed of a head, <%= xref "Operations", "an operation" %>, a body, and a tail. Furthermore, the directive may appear anywhere in the text.
|
56
|
+
|
57
|
+
In shorthand notation, the directive is composed of a head, <%= xref "Operations", "an operation" %> and a body. Furthermore, the directive may only appear in the text if it occupies an entire line.
|
58
|
+
|
59
|
+
In any case, directives are atomic constructs; they may not be nested.
|
60
|
+
|
61
|
+
%|section "Operations"
|
62
|
+
The first character that follows the head of a directive is known as an **operation**. Operations specify how the directive should be processed:
|
63
|
+
|
64
|
+
| Operation | Effect | Example |
|
65
|
+
| --------- | ------ | ------- |
|
66
|
+
| <%= Ember::Template::OPERATION_COMMENT_LINE %> | The entire directive is omitted from the output. | <%= xref "Comment directives" %> |
|
67
|
+
| <%= Ember::Template::OPERATION_EVAL_EXPRESSION %> | The body of the directive is evaluated as Ruby code, and the result of this evaluation is inserted into the output. | <%= xref "Vocal directives" %> |
|
68
|
+
| <%= Ember::Template::OPERATION_EVAL_TEMPLATE_STRING %> | The body of the directive is evaluated as an eRuby template, and the result of this evaluation is inserted into the output. | <%= xref "Dynamic template evaluation" %> |
|
69
|
+
| <%= Ember::Template::OPERATION_EVAL_TEMPLATE_FILE %> | The body of the directive is evaluated as Ruby code, and the result of this evaluation is assumed to be a string that specifies the path (either absolute or relative to the eRuby template file in which this directive is found) to a file containing an eRuby template. This file is read and its contents are evaluated as an eRuby template, and the result of this evaluation is inserted into the output. | <%= xref "Template file inclusion" %> |
|
70
|
+
| <%= Ember::Template::OPERATION_INSERT_PLAIN_FILE %> | The body of the directive is evaluated as Ruby code, and the result of this evaluation is assumed to be a string that specifies the path (either absolute or relative to the eRuby template file in which this directive is found) to a file. This file is read and its contents are inserted into the output. | <%= xref "Raw file inclusion" %> |
|
71
|
+
| | | The body of the directive is treated as the beginning of a Ruby block. The `do` keyword is automatically appended to the body of the directive if missing. | <%= xref "Block directives" %> |
|
72
|
+
| % | One "%" character is omitted from the head of the directive and the entire directive is inserted into the output. | <%= xref "Escaped directives" %> |
|
73
|
+
| (none of the above) | The body of the directive is evaluated as Ruby code, but the result of this evaluation *is not* inserted into the output. | <%= xref "Silent directives" %> |
|
74
|
+
|
75
|
+
<%
|
76
|
+
standard_directive = lambda do |body|
|
77
|
+
'<' + '%' + body + ' %' + '>'
|
78
|
+
end
|
79
|
+
|
80
|
+
shorthand_directive = lambda do |body|
|
81
|
+
'%' + body
|
82
|
+
end
|
83
|
+
|
84
|
+
template_example = lambda do |input, options|
|
85
|
+
input = input.strip.gsub(/^ {4}/, '') << "\n" # remove indentation
|
86
|
+
template = Ember::Template.new(input, options)
|
87
|
+
|
88
|
+
options_display =
|
89
|
+
if options.empty?
|
90
|
+
"This"
|
91
|
+
else
|
92
|
+
"With `#{options.inspect}`, this"
|
93
|
+
end
|
94
|
+
|
95
|
+
[
|
96
|
+
"<pre>#{input}</pre>",
|
97
|
+
|
98
|
+
"#{options_display} template compiles into:",
|
99
|
+
"<code>#{template.program}</code>",
|
100
|
+
|
101
|
+
"And renders as:",
|
102
|
+
"<pre>#{template.render}</pre>",
|
103
|
+
|
104
|
+
].join("\n\n")
|
105
|
+
end
|
106
|
+
%>
|
107
|
+
|
108
|
+
%|example "An empty template"
|
109
|
+
Begin with an empty template:
|
110
|
+
|
111
|
+
<%= template_example.call "", {} %>
|
112
|
+
|
113
|
+
%|example "Comment directives"
|
114
|
+
Add comment directives:
|
115
|
+
|
116
|
+
<%=
|
117
|
+
template_example.call %{
|
118
|
+
#{standard_directive['# this is a comment']}
|
119
|
+
#{shorthand_directive['# this is also a comment']}
|
120
|
+
|
121
|
+
#{standard_directive["# this is a\n\n multi-line comment"]}
|
122
|
+
}, :shorthand => true
|
123
|
+
%>
|
124
|
+
|
125
|
+
%|example "Escaped directives"
|
126
|
+
Add escaped directives:
|
127
|
+
|
128
|
+
<%=
|
129
|
+
example = '% this is an escaped directive'
|
130
|
+
|
131
|
+
template_example.call %{
|
132
|
+
#{standard_directive[example]}
|
133
|
+
#{shorthand_directive[example]}
|
134
|
+
}, :shorthand => true
|
135
|
+
%>
|
136
|
+
|
137
|
+
%|example "Vocal directives"
|
138
|
+
Add vocal directives, which produce output:
|
139
|
+
|
140
|
+
<%=
|
141
|
+
template_example.call %{
|
142
|
+
#{standard_directive['= "hello"']}
|
143
|
+
#{shorthand_directive['= "world"']}
|
144
|
+
|
145
|
+
}, :shorthand => true
|
146
|
+
%>
|
147
|
+
|
148
|
+
%|example "Silent directives"
|
149
|
+
Add silent directives, which do not produce output:
|
150
|
+
|
151
|
+
<%=
|
152
|
+
template_example.call %{
|
153
|
+
#{standard_directive[' a = "hello"']}
|
154
|
+
#{shorthand_directive[' b = "world"']}
|
155
|
+
|
156
|
+
#{standard_directive['= a']}
|
157
|
+
#{shorthand_directive['= b']}
|
158
|
+
|
159
|
+
}, :shorthand => true
|
160
|
+
%>
|
161
|
+
|
162
|
+
%|example "Block directives"
|
163
|
+
Add some Ruby blocks:
|
164
|
+
|
165
|
+
<%=
|
166
|
+
template_example.call %{
|
167
|
+
#{shorthand_directive[' words = %w[hello world]']}
|
168
|
+
|
169
|
+
#{standard_directive[' words.each do |w|']}
|
170
|
+
#{standard_directive['= w']}
|
171
|
+
#{standard_directive[' end']}
|
172
|
+
|
173
|
+
#{shorthand_directive[' words.each do |w|']}
|
174
|
+
#{shorthand_directive['= w']}
|
175
|
+
#{shorthand_directive[' end']}
|
176
|
+
|
177
|
+
#{shorthand_directive['|words.each |w|']}
|
178
|
+
#{shorthand_directive['= w']}
|
179
|
+
#{shorthand_directive[' end']}
|
180
|
+
|
181
|
+
}, :shorthand => true
|
182
|
+
%>
|
183
|
+
|
184
|
+
%|example "Infer block endings"
|
185
|
+
Omit <tt><%= standard_directive[' end'] %></tt> directives from the template:
|
186
|
+
|
187
|
+
<%=
|
188
|
+
template_example.call %{
|
189
|
+
#{shorthand_directive[' words = %w[hello world]']}
|
190
|
+
|
191
|
+
#{standard_directive[' words.each do |w|']}
|
192
|
+
#{standard_directive['= w']}
|
193
|
+
|
194
|
+
#{shorthand_directive[' words.each do |w|']}
|
195
|
+
#{shorthand_directive['= w']}
|
196
|
+
|
197
|
+
#{shorthand_directive['|words.each |w|']}
|
198
|
+
#{shorthand_directive['= w']}
|
199
|
+
|
200
|
+
}, :shorthand => true, :infer_end => true
|
201
|
+
%>
|
202
|
+
|
203
|
+
%|example "Raw file inclusion"
|
204
|
+
When <tt>doc/example.txt</tt> contains:
|
205
|
+
|
206
|
+
<pre><%< "example.txt" %></pre>
|
207
|
+
|
208
|
+
And the eRuby template is:
|
209
|
+
|
210
|
+
<%=
|
211
|
+
example = '< "example.txt"'
|
212
|
+
|
213
|
+
template_example.call %{
|
214
|
+
#{standard_directive[example]}
|
215
|
+
|
216
|
+
#{shorthand_directive[example]}
|
217
|
+
|
218
|
+
}, :shorthand => true, :source_file => __FILE__
|
219
|
+
%>
|
220
|
+
|
221
|
+
%|example "Template file inclusion"
|
222
|
+
When <tt>doc/example.erb</tt> contains:
|
223
|
+
|
224
|
+
<code lang="rhtml"><%< "example.erb" %></code>
|
225
|
+
|
226
|
+
And the eRuby template is:
|
227
|
+
|
228
|
+
<%=
|
229
|
+
example = '+ "example.erb"'
|
230
|
+
|
231
|
+
template_example.call %{
|
232
|
+
#{standard_directive[example]}
|
233
|
+
|
234
|
+
#{shorthand_directive[example]}
|
235
|
+
|
236
|
+
}, :shorthand => true, :source_file => __FILE__
|
237
|
+
%>
|
238
|
+
|
239
|
+
%|example "Dynamic template evaluation"
|
240
|
+
<%=
|
241
|
+
example = %{~ "#{shorthand_directive['= 2 + 2']}"}
|
242
|
+
|
243
|
+
template_example.call %{
|
244
|
+
#{standard_directive[example]}
|
245
|
+
|
246
|
+
#{shorthand_directive[example]}
|
247
|
+
|
248
|
+
}, :shorthand => true
|
249
|
+
%>
|
data/lib/ember.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright 2009 Suraj N. Kurapati
|
3
|
+
# See the LICENSE file for details.
|
4
|
+
#++
|
5
|
+
|
6
|
+
require 'rubygems'
|
7
|
+
gem 'inochi', '~> 1'
|
8
|
+
require 'inochi'
|
9
|
+
|
10
|
+
Inochi.init :Ember,
|
11
|
+
:version => '0.0.0',
|
12
|
+
:release => '2009-05-02',
|
13
|
+
:website => 'http://snk.tuxfamily.org/lib/ember',
|
14
|
+
:tagline => 'eRuby template processor'
|
15
|
+
|
16
|
+
require 'ember/template'
|
@@ -0,0 +1,586 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright 2009 Suraj N. Kurapati
|
3
|
+
# See the LICENSE file for details.
|
4
|
+
#++
|
5
|
+
|
6
|
+
require 'pathname'
|
7
|
+
|
8
|
+
module Ember
|
9
|
+
class Template
|
10
|
+
##
|
11
|
+
# Builds a processor that evaluates eRuby directives
|
12
|
+
# in the given input according to the given options.
|
13
|
+
#
|
14
|
+
# This processor transforms the given input into an
|
15
|
+
# executable Ruby program (provided by the #to_s() method)
|
16
|
+
# which is then executed by the #render() method on demand.
|
17
|
+
#
|
18
|
+
# eRuby directives that contribute to the output of
|
19
|
+
# the given template are called "vocal" directives.
|
20
|
+
# Those that do not are called "silent" directives.
|
21
|
+
#
|
22
|
+
# ==== Options
|
23
|
+
#
|
24
|
+
# [:result_variable]
|
25
|
+
# Name of the variable which stores the result of
|
26
|
+
# template evaluation during template evaluation.
|
27
|
+
#
|
28
|
+
# The default value is "_erbout".
|
29
|
+
#
|
30
|
+
# [:continue_result]
|
31
|
+
# Append to the result variable if it already exists?
|
32
|
+
#
|
33
|
+
# The default value is false.
|
34
|
+
#
|
35
|
+
# [:source_file]
|
36
|
+
# Name of the file which contains the given input. This
|
37
|
+
# is shown in stack traces when reporting error messages.
|
38
|
+
#
|
39
|
+
# The default value is "SOURCE".
|
40
|
+
#
|
41
|
+
# [:source_line]
|
42
|
+
# Line number at which the given input exists in the :source_file.
|
43
|
+
# This is shown in stack traces when reporting error messages.
|
44
|
+
#
|
45
|
+
# The default value is 1.
|
46
|
+
#
|
47
|
+
# [:shorthand]
|
48
|
+
# Treat lines beginning with "%" as eRuby directives?
|
49
|
+
#
|
50
|
+
# The default value is false.
|
51
|
+
#
|
52
|
+
# [:infer_end]
|
53
|
+
# Add missing <% end %> statements based on indentation?
|
54
|
+
#
|
55
|
+
# The default value is false.
|
56
|
+
#
|
57
|
+
# [:unindent]
|
58
|
+
# Unindent the content of eRuby blocks (everything
|
59
|
+
# between <% do %> ... <% end %>) hierarchically?
|
60
|
+
#
|
61
|
+
# The default value is false.
|
62
|
+
#
|
63
|
+
def initialize input, options = {}
|
64
|
+
@options = options
|
65
|
+
@compile = compile(input.to_s)
|
66
|
+
end
|
67
|
+
|
68
|
+
##
|
69
|
+
# Ruby source code assembled from the eRuby template
|
70
|
+
# provided as input to the constructor of this class.
|
71
|
+
#
|
72
|
+
def program
|
73
|
+
@compile
|
74
|
+
end
|
75
|
+
|
76
|
+
##
|
77
|
+
# Returns the result of executing the Ruby program for this template
|
78
|
+
# (provided by the #to_s() method) inside the given context binding.
|
79
|
+
#
|
80
|
+
def render(context = TOPLEVEL_BINDING)
|
81
|
+
eval @compile, context,
|
82
|
+
(@options[:source_file] || :SOURCE).to_s,
|
83
|
+
(@options[:source_line] || 1).to_i
|
84
|
+
end
|
85
|
+
|
86
|
+
class << self
|
87
|
+
##
|
88
|
+
# Builds a template whose body is read from the given source.
|
89
|
+
#
|
90
|
+
# If the source is a relative path, it will be resolved
|
91
|
+
# relative to options[:source_file] if that is a valid path.
|
92
|
+
#
|
93
|
+
def load_file path, options = {}
|
94
|
+
path = resolve_path(path, options)
|
95
|
+
new File.read(path), options.merge(:source_file => path)
|
96
|
+
end
|
97
|
+
|
98
|
+
##
|
99
|
+
# Returns the contents of the given file, which can be relative to
|
100
|
+
# the current template in which this command is being executed.
|
101
|
+
#
|
102
|
+
# If the source is a relative path, it will be resolved
|
103
|
+
# relative to options[:source_file] if that is a valid path.
|
104
|
+
#
|
105
|
+
def read_file path, options = {}
|
106
|
+
File.read resolve_path(path, options)
|
107
|
+
end
|
108
|
+
|
109
|
+
private
|
110
|
+
|
111
|
+
def resolve_path path, options = {}
|
112
|
+
unless Pathname.new(path).absolute?
|
113
|
+
if base = options[:source_file] and File.exist? base
|
114
|
+
# target is relative to the file in
|
115
|
+
# which the include directive exists
|
116
|
+
path = File.join(File.dirname(base), path)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
path
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
|
125
|
+
private
|
126
|
+
|
127
|
+
OPERATION_EVAL_EXPRESSION = '='
|
128
|
+
OPERATION_COMMENT_LINE = '#'
|
129
|
+
OPERATION_BEGIN_LAMBDA = '|'
|
130
|
+
OPERATION_EVAL_TEMPLATE_FILE = '+'
|
131
|
+
OPERATION_EVAL_TEMPLATE_STRING = '~'
|
132
|
+
OPERATION_INSERT_PLAIN_FILE = '<'
|
133
|
+
|
134
|
+
#:stopdoc:
|
135
|
+
|
136
|
+
OPERATIONS = [
|
137
|
+
OPERATION_COMMENT_LINE,
|
138
|
+
OPERATION_BEGIN_LAMBDA,
|
139
|
+
OPERATION_EVAL_EXPRESSION,
|
140
|
+
OPERATION_EVAL_TEMPLATE_FILE,
|
141
|
+
OPERATION_EVAL_TEMPLATE_STRING,
|
142
|
+
OPERATION_INSERT_PLAIN_FILE,
|
143
|
+
]
|
144
|
+
|
145
|
+
SILENT_OPERATIONS = [
|
146
|
+
OPERATION_COMMENT_LINE,
|
147
|
+
OPERATION_BEGIN_LAMBDA,
|
148
|
+
]
|
149
|
+
|
150
|
+
VOCAL_OPERATIONS = OPERATIONS - SILENT_OPERATIONS
|
151
|
+
|
152
|
+
DIRECTIVE_HEAD = '<%'
|
153
|
+
DIRECTIVE_BODY = '(?:(?#
|
154
|
+
there is nothing here before the alternation
|
155
|
+
because we want to match the "<%%>" base case
|
156
|
+
)|[^%](?:.(?!<%))*?)'
|
157
|
+
DIRECTIVE_TAIL = '-?%>'
|
158
|
+
|
159
|
+
SHORTHAND_HEAD = '%'
|
160
|
+
SHORTHAND_BODY = '(?:(?#
|
161
|
+
there is nothing here before the alternation
|
162
|
+
because we want to match the "<%%>" base case
|
163
|
+
)|[^%].*)'
|
164
|
+
SHORTHAND_TAIL = '$'
|
165
|
+
|
166
|
+
NEWLINE = '\r?\n'
|
167
|
+
SPACING = '[[:blank:]]*'
|
168
|
+
|
169
|
+
MARGIN_REGEXP = /^#{SPACING}(?=\S)/o
|
170
|
+
|
171
|
+
LAMBDA_BEGIN_REGEXP = /\b(do)\b\s*(\|.*?\|)?\s*$/
|
172
|
+
|
173
|
+
build_keyword_regexp = lambda {|*words| /\A\s*\b(#{words.join '|'})\b/ }
|
174
|
+
|
175
|
+
BLOCK_BEGIN_REGEXP = build_keyword_regexp[
|
176
|
+
# generic
|
177
|
+
:begin,
|
178
|
+
|
179
|
+
# conditional
|
180
|
+
:if, :unless, :case,
|
181
|
+
|
182
|
+
# loops
|
183
|
+
:for, :while, :until
|
184
|
+
]
|
185
|
+
|
186
|
+
BLOCK_CONTINUE_REGEXP = build_keyword_regexp[
|
187
|
+
# generic
|
188
|
+
:rescue, :ensure,
|
189
|
+
|
190
|
+
# conditional
|
191
|
+
:else, :elsif, :when
|
192
|
+
]
|
193
|
+
|
194
|
+
BLOCK_END_REGEXP = build_keyword_regexp[
|
195
|
+
# generic
|
196
|
+
:end
|
197
|
+
]
|
198
|
+
|
199
|
+
#:startdoc:
|
200
|
+
|
201
|
+
##
|
202
|
+
# Transforms the given eRuby template into an executable Ruby program.
|
203
|
+
#
|
204
|
+
def compile template
|
205
|
+
@program = Program.new(
|
206
|
+
@options[:result_variable] || :_erbout,
|
207
|
+
@options[:continue_result]
|
208
|
+
)
|
209
|
+
|
210
|
+
# convert "% at beginning of line" usage into <% normal %> usage
|
211
|
+
if @options[:shorthand]
|
212
|
+
i = 0
|
213
|
+
contents, directives =
|
214
|
+
template.split(/(#{DIRECTIVE_HEAD}#{DIRECTIVE_BODY}#{DIRECTIVE_TAIL})/mo).
|
215
|
+
partition { (i += 1) & 1 == 1 } # even/odd partition
|
216
|
+
|
217
|
+
# only process the content; do not touch the directives
|
218
|
+
# because they may contain code lines beginning with "%"
|
219
|
+
contents.each do |content|
|
220
|
+
# process unescaped directives
|
221
|
+
content.gsub! %r/^(#{SPACING})(#{SHORTHAND_HEAD}#{SHORTHAND_BODY})#{SHORTHAND_TAIL}/o, '\1<\2%>'
|
222
|
+
|
223
|
+
# unescape escaped directives
|
224
|
+
content.gsub! %r/^(#{SPACING})(#{SHORTHAND_HEAD})#{SHORTHAND_HEAD}/o, '\1\2'
|
225
|
+
end
|
226
|
+
|
227
|
+
template = contents.zip(directives).join
|
228
|
+
end
|
229
|
+
|
230
|
+
# translate template into Ruby code
|
231
|
+
@margins = []
|
232
|
+
@crowns = []
|
233
|
+
|
234
|
+
directive_matches = template.scan(/#{
|
235
|
+
'((%s)%s(%s)%s(%s)(%s?))' % [
|
236
|
+
SPACING,
|
237
|
+
DIRECTIVE_HEAD,
|
238
|
+
DIRECTIVE_BODY,
|
239
|
+
DIRECTIVE_TAIL,
|
240
|
+
SPACING,
|
241
|
+
NEWLINE,
|
242
|
+
]
|
243
|
+
}/mo)
|
244
|
+
|
245
|
+
directive_matches.each do |match|
|
246
|
+
# iteratively whittle the template
|
247
|
+
before_content, after_content = template.split(match[0], 2)
|
248
|
+
template = after_content
|
249
|
+
|
250
|
+
# process the raw content before the directive
|
251
|
+
process_content before_content
|
252
|
+
|
253
|
+
# process the directive itself
|
254
|
+
args = match + [after_content]
|
255
|
+
process_directive(*args)
|
256
|
+
end
|
257
|
+
|
258
|
+
# process remaining raw content *after* last directive
|
259
|
+
process_content template
|
260
|
+
|
261
|
+
# handle missing ends
|
262
|
+
if @options[:infer_end]
|
263
|
+
@margins.each { emit_end }
|
264
|
+
else
|
265
|
+
warn "There are at least #{@margins.length} missing '<% end %>' statements in the eRuby template." unless @margins.empty?
|
266
|
+
end
|
267
|
+
|
268
|
+
@program.compile
|
269
|
+
end
|
270
|
+
|
271
|
+
def close_block
|
272
|
+
raise 'cannot close unopened block' if @margins.empty?
|
273
|
+
@margins.pop
|
274
|
+
@crowns.pop
|
275
|
+
end
|
276
|
+
|
277
|
+
def emit_end
|
278
|
+
@program.emit_end
|
279
|
+
end
|
280
|
+
|
281
|
+
def infer_end line, skip_last_level = false
|
282
|
+
if @options[:infer_end] and
|
283
|
+
@program.new_line? and
|
284
|
+
not line.empty? and
|
285
|
+
current = line[MARGIN_REGEXP]
|
286
|
+
then
|
287
|
+
# number of levels to ascend
|
288
|
+
levels = @crowns.select {|previous| current <= previous }.length
|
289
|
+
|
290
|
+
# in the case of block-continuation and -ending directives,
|
291
|
+
# we must not ascend the very last (outmost) level at this
|
292
|
+
# point of the algorithm. that work will be done later on
|
293
|
+
levels -= 1 if skip_last_level
|
294
|
+
|
295
|
+
levels.times do |i|
|
296
|
+
p :infer => line if $DEBUG
|
297
|
+
close_block
|
298
|
+
emit_end
|
299
|
+
end
|
300
|
+
end
|
301
|
+
end
|
302
|
+
|
303
|
+
##
|
304
|
+
# Returns a new string containing the result of unindentation.
|
305
|
+
#
|
306
|
+
def unindent line
|
307
|
+
if @options[:unindent] and
|
308
|
+
@program.new_line? and
|
309
|
+
margin = @margins.last and
|
310
|
+
crown = @crowns.first
|
311
|
+
then
|
312
|
+
line.gsub(/^#{margin}/, crown)
|
313
|
+
else
|
314
|
+
line
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
def process_content content
|
319
|
+
content.split(/^/).each do |content_line|
|
320
|
+
# before_spacing
|
321
|
+
infer_end content_line, false
|
322
|
+
content_line = unindent(content_line)
|
323
|
+
|
324
|
+
# content + after_spacing
|
325
|
+
content_line.gsub! '<%%', '<%' # unescape escaped directives
|
326
|
+
@program.text content_line
|
327
|
+
|
328
|
+
# after_newline
|
329
|
+
@program.new_line if content_line =~ /\n\z/
|
330
|
+
end
|
331
|
+
end
|
332
|
+
|
333
|
+
def process_directive content_line, before_spacing, directive, after_spacing, after_newline, after_content
|
334
|
+
operation = directive[0, 1]
|
335
|
+
|
336
|
+
if OPERATIONS.include? operation
|
337
|
+
arguments = directive[1..-1]
|
338
|
+
else
|
339
|
+
operation = ''
|
340
|
+
arguments = directive
|
341
|
+
end
|
342
|
+
|
343
|
+
arguments = unindent(arguments)
|
344
|
+
|
345
|
+
is_vocal = VOCAL_OPERATIONS.include? operation
|
346
|
+
|
347
|
+
# before_spacing
|
348
|
+
after_margin = after_content[MARGIN_REGEXP]
|
349
|
+
|
350
|
+
open_block = lambda do
|
351
|
+
@margins << after_margin
|
352
|
+
@crowns << before_spacing
|
353
|
+
end
|
354
|
+
|
355
|
+
# '.' stands in place of the directive body,
|
356
|
+
# which may be empty in the case of '<%%>'
|
357
|
+
infer_end before_spacing + '.',
|
358
|
+
operation.empty? &&
|
359
|
+
arguments =~ BLOCK_END_REGEXP ||
|
360
|
+
arguments =~ BLOCK_CONTINUE_REGEXP
|
361
|
+
|
362
|
+
@program.text unindent(before_spacing) if is_vocal
|
363
|
+
|
364
|
+
# directive
|
365
|
+
template_class_name = '::Ember::Template'
|
366
|
+
nested_template_args = "(#{arguments}), #{@options.inspect}"
|
367
|
+
|
368
|
+
nest_template_with = lambda do |meth|
|
369
|
+
@program.code "#{template_class_name}.#{meth}(#{
|
370
|
+
nested_template_args
|
371
|
+
}.merge!(:continue_result => true)).render(binding)"
|
372
|
+
end
|
373
|
+
|
374
|
+
case operation
|
375
|
+
when OPERATION_EVAL_EXPRESSION
|
376
|
+
@program.expr arguments
|
377
|
+
|
378
|
+
when OPERATION_COMMENT_LINE
|
379
|
+
@program.code directive.gsub(/\S/, ' ')
|
380
|
+
|
381
|
+
when OPERATION_BEGIN_LAMBDA
|
382
|
+
arguments =~ /(\bdo\b)?\s*(\|[^\|]*\|)?\s*\z/
|
383
|
+
@program.code "#{$`} #{$1 || 'do'} #{$2}"
|
384
|
+
|
385
|
+
p :begin => directive if $DEBUG
|
386
|
+
open_block.call
|
387
|
+
|
388
|
+
when OPERATION_EVAL_TEMPLATE_STRING
|
389
|
+
nest_template_with[:new]
|
390
|
+
|
391
|
+
when OPERATION_EVAL_TEMPLATE_FILE
|
392
|
+
nest_template_with[:load_file]
|
393
|
+
|
394
|
+
when OPERATION_INSERT_PLAIN_FILE
|
395
|
+
@program.expr "#{template_class_name}.read_file(#{nested_template_args})"
|
396
|
+
|
397
|
+
else
|
398
|
+
@program.code arguments
|
399
|
+
|
400
|
+
unless arguments =~ /\n/ # don't bother parsing multi-line directives
|
401
|
+
case arguments
|
402
|
+
when BLOCK_BEGIN_REGEXP, LAMBDA_BEGIN_REGEXP
|
403
|
+
p :begin => directive if $DEBUG
|
404
|
+
open_block.call
|
405
|
+
|
406
|
+
when BLOCK_CONTINUE_REGEXP
|
407
|
+
# reopen because the new block might have a different margin
|
408
|
+
p :continue => directive if $DEBUG
|
409
|
+
close_block
|
410
|
+
open_block.call
|
411
|
+
|
412
|
+
when BLOCK_END_REGEXP
|
413
|
+
p :close => directive if $DEBUG
|
414
|
+
close_block
|
415
|
+
end
|
416
|
+
end
|
417
|
+
end
|
418
|
+
|
419
|
+
# after_spacing
|
420
|
+
@program.text after_spacing if is_vocal || after_newline.empty?
|
421
|
+
|
422
|
+
# after_newline
|
423
|
+
@program.text after_newline if is_vocal
|
424
|
+
@program.new_line unless after_newline.empty?
|
425
|
+
end
|
426
|
+
|
427
|
+
class Program
|
428
|
+
##
|
429
|
+
# Transforms this program into Ruby code which uses
|
430
|
+
# the given variable name as the evaluation buffer.
|
431
|
+
#
|
432
|
+
# If continue_result is true, the evaluation buffer is
|
433
|
+
# reused if it already exists in the rendering context.
|
434
|
+
#
|
435
|
+
def initialize result_variable, continue_result
|
436
|
+
@result_variable = result_variable
|
437
|
+
@continue_result = continue_result
|
438
|
+
@source_lines = [] # each line is composed of multiple statements
|
439
|
+
end
|
440
|
+
|
441
|
+
##
|
442
|
+
# Returns true if there are no source lines in this program.
|
443
|
+
#
|
444
|
+
def empty?
|
445
|
+
@source_lines.empty?
|
446
|
+
end
|
447
|
+
|
448
|
+
##
|
449
|
+
# Begins a new line in the program's source code.
|
450
|
+
#
|
451
|
+
def new_line
|
452
|
+
@source_lines << []
|
453
|
+
end
|
454
|
+
|
455
|
+
##
|
456
|
+
# Returns true if a new (blank) line is
|
457
|
+
# ready in the program's source code.
|
458
|
+
#
|
459
|
+
def new_line?
|
460
|
+
ary = insertion_point
|
461
|
+
ary.empty? || ary.all? {|stmt| stmt.type == :code }
|
462
|
+
end
|
463
|
+
|
464
|
+
##
|
465
|
+
# Schedules the given text to be inserted verbatim
|
466
|
+
# into the evaluation buffer when this program is run.
|
467
|
+
#
|
468
|
+
def text value
|
469
|
+
# don't bother emitting empty strings
|
470
|
+
return if value.empty?
|
471
|
+
|
472
|
+
# combine adjacent statements to reduce code size
|
473
|
+
if prev = insertion_point.last and prev.type == :text
|
474
|
+
prev.value << value
|
475
|
+
else
|
476
|
+
statement :text, value
|
477
|
+
end
|
478
|
+
end
|
479
|
+
|
480
|
+
##
|
481
|
+
# Schedules the given Ruby code to be
|
482
|
+
# evaluated when this program is run.
|
483
|
+
#
|
484
|
+
def code value
|
485
|
+
statement :code, value
|
486
|
+
end
|
487
|
+
|
488
|
+
##
|
489
|
+
# Schedules the given Ruby code to be evaluated and inserted
|
490
|
+
# into the evaluation buffer when this program is run.
|
491
|
+
#
|
492
|
+
def expr value
|
493
|
+
statement :expr, value
|
494
|
+
end
|
495
|
+
|
496
|
+
##
|
497
|
+
# Inserts an <% end %> directive before the
|
498
|
+
# oldest non-whitespace statement possible.
|
499
|
+
#
|
500
|
+
# Preceding lines that only emit whitespace are skipped.
|
501
|
+
#
|
502
|
+
def emit_end
|
503
|
+
ending = Statement.new(:code, :end)
|
504
|
+
current = insertion_point
|
505
|
+
|
506
|
+
can_skip_line = lambda do |line|
|
507
|
+
line.empty? ||
|
508
|
+
line.all? {|stmt| stmt.type == :text && stmt.value =~ /\A\s*\z/ }
|
509
|
+
end
|
510
|
+
|
511
|
+
if can_skip_line[current]
|
512
|
+
target = current
|
513
|
+
|
514
|
+
# skip past empty whitespace in previous lines
|
515
|
+
@source_lines.reverse_each do |line|
|
516
|
+
break unless can_skip_line[line]
|
517
|
+
target = line
|
518
|
+
end
|
519
|
+
|
520
|
+
target.unshift ending
|
521
|
+
else
|
522
|
+
current.push ending
|
523
|
+
end
|
524
|
+
end
|
525
|
+
|
526
|
+
##
|
527
|
+
# Transforms this program into executable Ruby source code.
|
528
|
+
#
|
529
|
+
def compile
|
530
|
+
'(%s %s []; %s; %s.join)' % [
|
531
|
+
@result_variable,
|
532
|
+
@continue_result ? '||=' : '=',
|
533
|
+
|
534
|
+
@source_lines.map do |source_line|
|
535
|
+
compiled_line = []
|
536
|
+
combine_prev = false
|
537
|
+
|
538
|
+
source_line.each do |stmt|
|
539
|
+
is_code = stmt.type == :code
|
540
|
+
is_expr = stmt.type == :expr
|
541
|
+
|
542
|
+
if is_code
|
543
|
+
compiled_line << stmt.value
|
544
|
+
combine_prev = false
|
545
|
+
|
546
|
+
else
|
547
|
+
code =
|
548
|
+
if is_expr
|
549
|
+
" << (#{stmt.value})"
|
550
|
+
else
|
551
|
+
" << #{stmt.value.inspect}"
|
552
|
+
end
|
553
|
+
|
554
|
+
if combine_prev
|
555
|
+
compiled_line.last << code
|
556
|
+
else
|
557
|
+
compiled_line << @result_variable.to_s + code
|
558
|
+
end
|
559
|
+
|
560
|
+
combine_prev = true
|
561
|
+
end
|
562
|
+
end
|
563
|
+
|
564
|
+
compiled_line.join('; ')
|
565
|
+
|
566
|
+
end.join("\n"),
|
567
|
+
|
568
|
+
@result_variable,
|
569
|
+
]
|
570
|
+
end
|
571
|
+
|
572
|
+
private
|
573
|
+
|
574
|
+
def insertion_point
|
575
|
+
new_line if empty?
|
576
|
+
@source_lines.last
|
577
|
+
end
|
578
|
+
|
579
|
+
def statement *args
|
580
|
+
insertion_point << Statement.new(*args)
|
581
|
+
end
|
582
|
+
|
583
|
+
Statement = Struct.new :type, :value
|
584
|
+
end
|
585
|
+
end
|
586
|
+
end
|