fontcustom 1.0.1 → 1.1.0.pre

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +8 -0
  3. data/README.md +1 -1
  4. data/TODO.md +6 -0
  5. data/lib/fontcustom.rb +1 -1
  6. data/lib/fontcustom/actions.rb +28 -0
  7. data/lib/fontcustom/cli.rb +16 -15
  8. data/lib/fontcustom/generator/font.rb +31 -35
  9. data/lib/fontcustom/generator/template.rb +49 -34
  10. data/lib/fontcustom/options.rb +17 -11
  11. data/lib/fontcustom/scripts/generate.py +1 -1
  12. data/lib/fontcustom/templates/_fontcustom-bootstrap-ie7.scss +4 -3
  13. data/lib/fontcustom/templates/_fontcustom-bootstrap.scss +9 -14
  14. data/lib/fontcustom/templates/_fontcustom.scss +8 -14
  15. data/lib/fontcustom/templates/fontcustom-bootstrap-ie7.css +3 -2
  16. data/lib/fontcustom/templates/fontcustom-bootstrap.css +9 -8
  17. data/lib/fontcustom/templates/fontcustom-preview.html +2 -3
  18. data/lib/fontcustom/templates/fontcustom.css +7 -7
  19. data/lib/fontcustom/templates/fontcustom.yml +25 -66
  20. data/lib/fontcustom/util.rb +108 -31
  21. data/lib/fontcustom/version.rb +1 -1
  22. data/lib/fontcustom/watcher.rb +24 -8
  23. data/spec/fixtures/{mixed-output → generators}/.fontcustom-data +0 -0
  24. data/spec/fixtures/{mixed-output → generators/mixed-output}/another-font.ttf +0 -0
  25. data/spec/fixtures/{mixed-output → generators/mixed-output}/dont-delete-me.bro +0 -0
  26. data/spec/fixtures/{mixed-output → generators/mixed-output}/fontcustom.css +0 -0
  27. data/spec/fixtures/{mixed-output → generators/mixed-output}/fontcustom_cc5ce52f2ae4f9ce2e7ee8131bbfee1e.eot +0 -0
  28. data/spec/fixtures/{mixed-output → generators/mixed-output}/fontcustom_cc5ce52f2ae4f9ce2e7ee8131bbfee1e.svg +0 -0
  29. data/spec/fixtures/{mixed-output → generators/mixed-output}/fontcustom_cc5ce52f2ae4f9ce2e7ee8131bbfee1e.ttf +0 -0
  30. data/spec/fixtures/{mixed-output → generators/mixed-output}/fontcustom_cc5ce52f2ae4f9ce2e7ee8131bbfee1e.woff +0 -0
  31. data/spec/fixtures/{not-a-dir → shared/not-a-dir} +0 -0
  32. data/spec/fixtures/shared/templates/custom.css +1 -0
  33. data/spec/fixtures/shared/templates/regular.css +1 -0
  34. data/spec/fixtures/{empty → shared/vectors-empty}/no_vectors_here.txt +0 -0
  35. data/spec/fixtures/{vectors → shared/vectors}/C.svg +0 -0
  36. data/spec/fixtures/{vectors → shared/vectors}/D.svg +0 -0
  37. data/spec/fixtures/vectors/a_R3ally-eXotic f1Le Name.svg b/data/spec/fixtures/shared/vectors/a_R3ally-eXotic f1Le → Name.svg +0 -0
  38. data/spec/fixtures/util/config-is-in-dir/fontcustom.yml +1 -0
  39. data/spec/fixtures/util/fontcustom-malformed.yml +1 -0
  40. data/spec/fixtures/{fontcustom.yml → util/fontcustom.yml} +0 -0
  41. data/spec/fixtures/util/rails-like/config/fontcustom.yml +1 -0
  42. data/spec/fontcustom/actions_spec.rb +22 -0
  43. data/spec/fontcustom/generator/font_spec.rb +107 -62
  44. data/spec/fontcustom/generator/template_spec.rb +124 -29
  45. data/spec/fontcustom/util_spec.rb +299 -53
  46. data/spec/fontcustom/watcher_spec.rb +56 -10
  47. data/spec/spec_helper.rb +8 -3
  48. metadata +46 -34
  49. data/spec/fixtures/empty-data/.fontcustom-data +0 -0
@@ -13,101 +13,347 @@ describe Fontcustom::Util do
13
13
  end
14
14
 
15
15
  context ".collect_options" do
16
- it "should return defaults when called without arguments" do
17
- options = util.collect_options
18
- defaults = Fontcustom::DEFAULT_OPTIONS.dup
19
-
20
- # ignore :templates and :output since they're generated
21
- options.delete(:templates)
22
- defaults.delete(:templates)
23
- options.delete(:output)
24
- defaults.delete(:output)
25
-
26
- options.should == defaults
16
+ it "should raise error if fontcustom.yml isn't valid" do
17
+ options = {
18
+ :project_root => fixture,
19
+ :input => "shared/vectors",
20
+ :config => "util/fontcustom-malformed.yml"
21
+ }
22
+ expect { util.collect_options(options) }.to raise_error Fontcustom::Error, /couldn't read your configuration/
27
23
  end
28
24
 
29
25
  it "should overwrite defaults with config file" do
30
- options = util.collect_options :config => fixture("fontcustom.yml")
31
- options[:font_name].should == "custom-name-from-config"
26
+ options = {
27
+ :project_root => fixture,
28
+ :input => "shared/vectors",
29
+ :config => "util/fontcustom.yml"
30
+ }
31
+ options = util.collect_options options
32
+ options[:font_name].should == "Custom-Name-From-Config"
32
33
  end
33
34
 
34
35
  it "should overwrite config file and defaults with CLI options" do
35
- options = util.collect_options :config => fixture("fontcustom.yml"), :font_name => "custom-name-from-cli"
36
+ options = {
37
+ :project_root => fixture,
38
+ :input => "shared/vectors",
39
+ :font_name => "custom-name-from-cli",
40
+ :config => "util/fontcustom.yml"
41
+ }
42
+ options = util.collect_options options
36
43
  options[:font_name].should == "custom-name-from-cli"
37
44
  end
38
45
 
39
46
  it "should normalize file name" do
40
- options = util.collect_options :font_name => " A_stR4nG3 nAm3 "
41
- options[:font_name].should == "a_str4ng3-nam3"
47
+ options = {
48
+ :project_root => fixture,
49
+ :input => "shared/vectors",
50
+ :font_name => " A_stR4nG3 nAm3 Ø& "
51
+ }
52
+ options = util.collect_options options
53
+ options[:font_name].should == "A_stR4nG3--nAm3---"
42
54
  end
43
55
  end
44
56
 
45
57
  context ".get_config_path" do
46
58
  it "should search for fontcustom.yml if options[:config] is a dir" do
47
- options = { :config => fixture("") }
48
- util.get_config_path(options).should == fixture("fontcustom.yml")
59
+ options = {
60
+ :project_root => fixture,
61
+ :config => "util/config-is-in-dir"
62
+ }
63
+ util.get_config_path(options).should == fixture("util/config-is-in-dir/fontcustom.yml")
64
+ end
65
+
66
+ it "should use options[:config] if it's a file" do
67
+ options = {
68
+ :project_root => fixture,
69
+ :config => "util/fontcustom.yml"
70
+ }
71
+ util.get_config_path(options).should == fixture("util/fontcustom.yml")
72
+ end
73
+
74
+ it "should find fontcustom.yml in :project_root/config" do
75
+ options = { :project_root => fixture("util/rails-like") }
76
+ util.get_config_path(options).should == fixture("util/rails-like/config/fontcustom.yml")
77
+ end
78
+
79
+ it "should follow ../../ paths" do
80
+ options = {
81
+ :project_root => fixture("shared"),
82
+ :input => "vectors",
83
+ :config => "../util"
84
+ }
85
+ util.get_config_path(options).should == fixture("util/fontcustom.yml")
86
+ end
87
+
88
+ it "should print out which fontcustom.yml it's using"
89
+
90
+ it "should raise error if fontcustom.yml was specified but doesn't exist" do
91
+ options = {
92
+ :project_root => fixture,
93
+ :input => "shared/vectors",
94
+ :config => "does-not-exist"
95
+ }
96
+ expect { util.get_config_path(options) }.to raise_error Fontcustom::Error, /couldn't find/
97
+ end
98
+
99
+ it "should print a warning if fontcustom.yml was NOT specified and doesn't exist"
100
+ end
101
+
102
+ context ".get_input_paths" do
103
+ it "should raise error if input[:vectors] doesn't contain vectors" do
104
+ options = {
105
+ :project_root => fixture,
106
+ :input => "shared/vectors-empty"
107
+ }
108
+ expect { util.get_input_paths(options) }.to raise_error Fontcustom::Error, /doesn't contain any vectors/
109
+ end
110
+
111
+ it "should follow ../../ paths" do
112
+ options = {
113
+ :project_root => fixture("util"),
114
+ :input => {:vectors => "../shared/vectors", :templates => "../shared/templates"}
115
+ }
116
+ paths = util.get_input_paths(options)
117
+ paths[:vectors].should eq(fixture("shared/vectors"))
118
+ paths[:templates].should eq(fixture("shared/templates"))
119
+ end
120
+
121
+ context "when passed a hash" do
122
+ it "should return a hash of input locations" do
123
+ options = {
124
+ :input => { :vectors => "shared/vectors" },
125
+ :project_root => fixture
126
+ }
127
+ paths = util.get_input_paths(options)
128
+ paths.should have_key("vectors")
129
+ paths.should have_key("templates")
130
+ end
131
+
132
+ it "should set :templates as :vectors if :templates isn't passed" do
133
+ options = {
134
+ :input => { :vectors => "shared/vectors" },
135
+ :project_root => fixture
136
+ }
137
+ paths = util.get_input_paths(options)
138
+ paths[:vectors].should equal(paths[:templates])
139
+ end
140
+
141
+ it "should preserve :templates if it is passed" do
142
+ options = {
143
+ :input => { :vectors => "shared/vectors", :templates => "shared/templates" },
144
+ :project_root => fixture
145
+ }
146
+ paths = util.get_input_paths(options)
147
+ paths[:templates].should_not equal(paths[:vectors])
148
+ end
149
+
150
+ it "should raise an error if :vectors isn't included" do
151
+ options = {
152
+ :input => { :templates => "shared/templates" },
153
+ :project_root => fixture
154
+ }
155
+ expect { util.get_input_paths(options) }.to raise_error Fontcustom::Error, /should be a string or a hash/
156
+ end
157
+
158
+ it "should raise an error if :vectors doesn't point to an existing directory" do
159
+ options = {
160
+ :input => { :vectors => "shared/not-a-dir" },
161
+ :project_root => fixture
162
+ }
163
+ expect { util.get_input_paths(options) }.to raise_error Fontcustom::Error, /should be a directory/
164
+ end
49
165
  end
50
166
 
51
- it "should search use options[:config] if it's a file" do
52
- options = { :config => fixture("fontcustom.yml") }
53
- util.get_config_path(options).should == fixture("fontcustom.yml")
167
+ context "when passed a string" do
168
+ it "should return a hash of input locations" do
169
+ options = {
170
+ :input => "shared/vectors",
171
+ :project_root => fixture
172
+ }
173
+ paths = util.get_input_paths(options)
174
+ paths.should have_key("vectors")
175
+ paths.should have_key("templates")
176
+ end
177
+
178
+ it "should set :templates to match :vectors" do
179
+ options = {
180
+ :input => "shared/vectors",
181
+ :project_root => fixture
182
+ }
183
+ paths = util.get_input_paths(options)
184
+ paths[:vectors].should equal(paths[:templates])
185
+ end
186
+
187
+ it "should raise an error if :vectors doesn't point to a directory" do
188
+ options = {
189
+ :input => "shared/not-a-dir",
190
+ :project_root => fixture
191
+ }
192
+ expect { util.collect_options options }.to raise_error Fontcustom::Error, /should be a directory/
193
+ end
54
194
  end
195
+ end
55
196
 
56
- it "should search in input dir if no options[:config] is given" do
57
- options = { :input => fixture("") }
58
- util.get_config_path(options).should == fixture("fontcustom.yml")
197
+ context ".get_output_paths" do
198
+ it "should default to :project_root/:font_name if no output is specified" do
199
+ options = { :project_root => fixture, :font_name => "test" }
200
+ paths = util.get_output_paths(options)
201
+ paths[:fonts].should eq(fixture("test"))
59
202
  end
60
203
 
61
- it "should return false if neither exist" do
62
- options = { :input => fixture("vectors") }
63
- util.get_config_path(options).should be_false
204
+ it "should print a warning when defaulting to :project_root/:font_name"
205
+
206
+ it "should follow ../../ paths" do
207
+ options = {
208
+ :project_root => fixture("shared"),
209
+ :input => "vectors",
210
+ :output => {
211
+ :fonts => "../output/fonts",
212
+ :css => "../output/css",
213
+ :preview => "../output/views"
214
+ }
215
+ }
216
+ paths = util.get_output_paths(options)
217
+ paths[:fonts].should eq(fixture("output/fonts"))
218
+ paths[:css].should eq(fixture("output/css"))
219
+ paths[:preview].should eq(fixture("output/views"))
220
+ end
221
+
222
+ context "when passed a hash" do
223
+ it "should return a hash of output locations" do
224
+ options = {
225
+ :output => { :fonts => "output/fonts" },
226
+ :project_root => fixture
227
+ }
228
+ paths = util.get_output_paths(options)
229
+ paths.should have_key("fonts")
230
+ paths.should have_key("css")
231
+ paths.should have_key("preview")
232
+ end
233
+
234
+ it "should set :css and :preview to match :fonts if either aren't passed" do
235
+ options = {
236
+ :output => { :fonts => "output/fonts" },
237
+ :project_root => fixture
238
+ }
239
+ paths = util.get_output_paths(options)
240
+ paths[:css].should equal(paths[:fonts])
241
+ paths[:preview].should equal(paths[:fonts])
242
+ end
243
+
244
+ it "should preserve :css and :preview if they do exist" do
245
+ options = {
246
+ :output => {
247
+ :fonts => "output/fonts",
248
+ :css => "output/styles",
249
+ :preview => "output/preview"
250
+ },
251
+ :project_root => fixture
252
+ }
253
+ paths = util.get_output_paths(options)
254
+ paths[:css].should_not equal(paths[:fonts])
255
+ paths[:preview].should_not equal(paths[:fonts])
256
+ end
257
+
258
+ it "should create additional paths if they are given" do
259
+ options = {
260
+ :output => {
261
+ :fonts => "output/fonts",
262
+ "special.js" => "assets/javascripts"
263
+ },
264
+ :project_root => fixture
265
+ }
266
+ paths = util.get_output_paths(options)
267
+ paths["special.js"].should eq(File.join(options[:project_root], "assets/javascripts"))
268
+ end
269
+
270
+ it "should raise an error if :fonts isn't included" do
271
+ options = {
272
+ :output => { :css => "output/styles" },
273
+ :project_root => fixture
274
+ }
275
+ expect { util.get_output_paths(options) }.to raise_error Fontcustom::Error, /containing a "fonts" key/
276
+ end
277
+ end
278
+
279
+ context "when passed a string" do
280
+ it "should return a hash of output locations" do
281
+ options = {
282
+ :output => "output/fonts",
283
+ :project_root => fixture
284
+ }
285
+ paths = util.get_output_paths(options)
286
+ paths.should have_key("fonts")
287
+ paths.should have_key("css")
288
+ paths.should have_key("preview")
289
+ end
290
+
291
+ it "should set :css and :preview to match :fonts" do
292
+ options = {
293
+ :output => "output/fonts",
294
+ :project_root => fixture
295
+ }
296
+ paths = util.get_output_paths(options)
297
+ paths[:css].should equal(paths[:fonts])
298
+ paths[:preview].should equal(paths[:fonts])
299
+ end
300
+
301
+ it "should raise an error if :fonts exists but isn't a directory" do
302
+ options = {
303
+ :output => "shared/not-a-dir",
304
+ :project_root => fixture
305
+ }
306
+ expect { util.get_output_paths(options) }.to raise_error Fontcustom::Error, /directory, not a file/
307
+ end
64
308
  end
65
309
  end
66
310
 
67
- context ".get_template_paths" do
311
+ context ".get_templates" do
68
312
  it "should ensure that 'css' is included with 'preview'" do
69
313
  lib = util.gem_lib_path
70
- options = { :input => fixture("vectors"), :templates => %W|preview| }
71
- templates = util.get_template_paths options
314
+ options = { :input => fixture("shared/vectors"), :templates => %W|preview| }
315
+ templates = util.get_templates options
72
316
  templates.should =~ [
73
- File.join(lib, "templates", "fontcustom.css"),
317
+ File.join(lib, "templates", "fontcustom.css"),
74
318
  File.join(lib, "templates", "fontcustom-preview.html")
75
319
  ]
76
320
  end
77
321
 
78
322
  it "should expand shorthand for packaged templates" do
79
323
  lib = util.gem_lib_path
80
- options = { :input => fixture("vectors"), :templates => %W|preview css scss bootstrap bootstrap-scss bootstrap-ie7 bootstrap-ie7-scss| }
81
- templates = util.get_template_paths options
324
+ options = { :input => fixture("shared/vectors"), :templates => %W|preview css scss bootstrap bootstrap-scss bootstrap-ie7 bootstrap-ie7-scss| }
325
+ templates = util.get_templates options
82
326
  templates.should =~ [
83
327
  File.join(lib, "templates", "fontcustom-preview.html"),
84
- File.join(lib, "templates", "fontcustom.css"),
328
+ File.join(lib, "templates", "fontcustom.css"),
85
329
  File.join(lib, "templates", "_fontcustom.scss"),
86
- File.join(lib, "templates", "fontcustom-bootstrap.css"),
87
- File.join(lib, "templates", "_fontcustom-bootstrap.scss"),
88
- File.join(lib, "templates", "fontcustom-bootstrap-ie7.css"),
89
- File.join(lib, "templates", "_fontcustom-bootstrap-ie7.scss")
330
+ File.join(lib, "templates", "fontcustom-bootstrap.css"),
331
+ File.join(lib, "templates", "_fontcustom-bootstrap.scss"),
332
+ File.join(lib, "templates", "fontcustom-bootstrap-ie7.css"),
333
+ File.join(lib, "templates", "_fontcustom-bootstrap-ie7.scss")
90
334
  ]
91
335
  end
92
336
 
93
- it "should search in Dir.pwd first" do
94
- Dir.chdir fixture("")
95
- options = { :templates => %W|not-a-dir| }
96
- templates = util.get_template_paths options
97
- templates.should =~ ["not-a-dir"]
98
- end
99
-
100
- it "should search in options[:input] second" do
101
- options = { :input => fixture("empty"), :templates => %W|no_vectors_here.txt| }
102
- templates = util.get_template_paths options
103
- templates.should =~ [fixture("empty/no_vectors_here.txt")]
337
+ it "should find custom templates in :template_path" do
338
+ options = {
339
+ :project_root => fixture,
340
+ :input => {
341
+ :vectors => fixture("shared/vectors"),
342
+ :templates => fixture("shared/templates")
343
+ },
344
+ :templates => %W|custom.css|
345
+ }
346
+ templates = util.get_templates options
347
+ templates.should eq([ fixture("shared/templates/custom.css") ])
104
348
  end
105
349
 
106
350
  it "should raise an error if a template does not exist" do
107
- options = { :input => fixture("vectors"), :templates => %W|css #{fixture("fake-template")}| }
108
- expect { util.get_template_paths options }.to raise_error(
109
- Fontcustom::Error, /couldn't find.+#{fixture("fake-template")}/
110
- )
351
+ options = {
352
+ :project_root => fixture,
353
+ :input => { :templates => "shared/templates" },
354
+ :templates => %W|css fake-template|
355
+ }
356
+ expect { util.get_templates options }.to raise_error Fontcustom::Error, /couldn't find.+fake-template/
111
357
  end
112
358
  end
113
359
  end
@@ -7,7 +7,7 @@ describe Fontcustom::Watcher do
7
7
  Fontcustom::Generator::Font.stub :start
8
8
  Fontcustom::Generator::Template.stub :start
9
9
  opts = Fontcustom::Util.collect_options options
10
- opts[:blocking] = false # undocumented — non-blocking use of watcher
10
+ opts[:blocking] = false # undocumented — non-blocking use of watcher for testing
11
11
  Fontcustom::Watcher.new opts
12
12
  end
13
13
 
@@ -15,7 +15,11 @@ describe Fontcustom::Watcher do
15
15
  it "should call generators on init" do
16
16
  Fontcustom::Generator::Font.should_receive(:start).once
17
17
  Fontcustom::Generator::Template.should_receive(:start).once
18
- w = watcher :input => fixture("vectors"), :output => fixture("watcher-test")
18
+ w = watcher(
19
+ :project_root => fixture,
20
+ :input => "shared/vectors",
21
+ :output => "output"
22
+ )
19
23
  # silence output
20
24
  capture(:stdout) do
21
25
  w.watch
@@ -26,7 +30,12 @@ describe Fontcustom::Watcher do
26
30
  it "should not call generators on init if options[:skip_first] is passed" do
27
31
  Fontcustom::Generator::Font.should_not_receive(:start)
28
32
  Fontcustom::Generator::Template.should_not_receive(:start)
29
- w = watcher :input => fixture("vectors"), :output => fixture("watcher-test"), :skip_first => true
33
+ w = watcher(
34
+ :project_root => fixture,
35
+ :input => "shared/vectors",
36
+ :output => "output",
37
+ :skip_first => true
38
+ )
30
39
  capture(:stdout) do
31
40
  w.watch
32
41
  w.stop
@@ -36,31 +45,68 @@ describe Fontcustom::Watcher do
36
45
  it "should call generators when vectors change" do
37
46
  Fontcustom::Generator::Font.should_receive(:start).once
38
47
  Fontcustom::Generator::Template.should_receive(:start).once
39
- w = watcher :input => fixture("vectors"), :output => fixture("watcher-test"), :skip_first => true
48
+ w = watcher(
49
+ :project_root => fixture,
50
+ :input => "shared/vectors",
51
+ :output => "output",
52
+ :skip_first => true
53
+ )
40
54
  capture(:stdout) do
41
55
  begin
42
56
  w.watch
43
- FileUtils.cp fixture("vectors/C.svg"), fixture("vectors/test.svg")
44
- sleep 2
57
+ FileUtils.cp fixture("shared/vectors/C.svg"), fixture("shared/vectors/test.svg")
58
+ sleep 1
45
59
  ensure
46
60
  w.stop
47
- new = fixture("vectors/test.svg")
61
+ new = fixture("shared/vectors/test.svg")
48
62
  FileUtils.rm(new) if File.exists?(new)
49
63
  end
50
64
  end
51
65
  end
52
66
 
67
+ it "should call generators when watched templates change" do
68
+ Fontcustom::Generator::Font.should_receive(:start).once
69
+ Fontcustom::Generator::Template.should_receive(:start).once
70
+ w = watcher(
71
+ :project_root => fixture,
72
+ :input => {:vectors => "shared/vectors", :templates => "shared/templates"},
73
+ :templates => %w|css preview custom.css|,
74
+ :output => "output",
75
+ :skip_first => true
76
+ )
77
+ capture(:stdout) do
78
+ begin
79
+ template = fixture "shared/templates/custom.css"
80
+ content = File.read template
81
+ new = content + "\n.bar { color: red; }"
82
+
83
+ w.watch
84
+ File.open(template, "w") { |file| file.write(new) }
85
+ sleep 1
86
+ ensure
87
+ w.stop
88
+ File.open(template, "w") { |file| file.write(content) }
89
+ end
90
+ end
91
+
92
+ end
93
+
53
94
  it "should do nothing when non-vectors change" do
54
95
  Fontcustom::Generator::Font.should_not_receive(:start)
55
96
  Fontcustom::Generator::Template.should_not_receive(:start)
56
- w = watcher :input => fixture("vectors"), :output => fixture("watcher-test"), :skip_first => true
97
+ w = watcher(
98
+ :project_root => fixture,
99
+ :input => "shared/vectors",
100
+ :output => "output",
101
+ :skip_first => true
102
+ )
57
103
  capture(:stdout) do
58
104
  begin
59
105
  w.watch
60
- FileUtils.touch fixture("vectors/non-vector-file")
106
+ FileUtils.touch fixture("shared/vectors/non-vector-file")
61
107
  ensure
62
108
  w.stop
63
- new = fixture("vectors/non-vector-file")
109
+ new = fixture("shared/vectors/non-vector-file")
64
110
  FileUtils.rm(new) if File.exists?(new)
65
111
  end
66
112
  end