tilt-handlebars 1.2.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. checksums.yaml +5 -5
  2. data/.editorconfig +12 -0
  3. data/.github/workflows/ci.yml +76 -0
  4. data/.github/workflows/publish.yml +58 -0
  5. data/.gitignore +14 -15
  6. data/.rspec +1 -0
  7. data/.rubocop.yml +189 -0
  8. data/Appraisals +9 -0
  9. data/CONTRIBUTING.md +75 -0
  10. data/Gemfile +3 -1
  11. data/LICENSE +22 -0
  12. data/README.md +34 -19
  13. data/RELEASE_NOTES.md +50 -8
  14. data/Rakefile +4 -8
  15. data/bin/audit +8 -0
  16. data/bin/build +8 -0
  17. data/bin/bundle +114 -0
  18. data/bin/bundler-audit +29 -0
  19. data/bin/console +15 -0
  20. data/bin/doc +7 -0
  21. data/bin/lint +8 -0
  22. data/bin/rake +29 -0
  23. data/bin/release +25 -0
  24. data/bin/rspec +29 -0
  25. data/bin/rubocop +29 -0
  26. data/bin/setup +13 -0
  27. data/bin/tag +25 -0
  28. data/bin/test +7 -0
  29. data/bin/version +24 -0
  30. data/gemfiles/tilt_1.gemfile +7 -0
  31. data/gemfiles/tilt_2.gemfile +7 -0
  32. data/lib/sinatra/handlebars.rb +10 -6
  33. data/lib/tilt/handlebars/version.rb +3 -1
  34. data/lib/tilt/handlebars.rb +57 -46
  35. data/lib/tilt-handlebars.rb +3 -0
  36. data/spec/fixtures/helpers.rb +21 -0
  37. data/spec/fixtures/views/hello.hbs +1 -0
  38. data/spec/fixtures/views/hello_missing.hbs +1 -0
  39. data/spec/fixtures/views/hello_partial.hbs +1 -0
  40. data/spec/fixtures/views/hello_partial2.hbs +1 -0
  41. data/spec/fixtures/views/partial.hbs +1 -0
  42. data/spec/fixtures/views/partial2.handlebars +1 -0
  43. data/spec/sinatra/handlebars_spec.rb +47 -0
  44. data/spec/spec_coverage.rb +19 -0
  45. data/spec/spec_helper.rb +23 -0
  46. data/spec/tilt/handlebars_template_spec.rb +543 -0
  47. data/spec/tilt_spec.rb +40 -0
  48. data/tasks/doc.rake +8 -0
  49. data/tasks/gem.rake +7 -0
  50. data/tasks/lint.rake +7 -0
  51. data/tasks/test.rake +10 -0
  52. data/tilt-handlebars.gemspec +48 -23
  53. metadata +261 -65
  54. data/.travis.yml +0 -6
  55. data/LICENSE.txt +0 -22
  56. data/test/fixtures/views/hello.hbs +0 -1
  57. data/test/fixtures/views/missing_partial.hbs +0 -1
  58. data/test/fixtures/views/partial.hbs +0 -1
  59. data/test/fixtures/views/partial2.handlebars +0 -1
  60. data/test/fixtures/views/partial_test.hbs +0 -1
  61. data/test/fixtures/views/partial_test2.handlebars +0 -1
  62. data/test/fixtures/views/two_partials.hbs +0 -1
  63. data/test/sinatra_test.rb +0 -38
  64. data/test/test_helper.rb +0 -9
  65. data/test/tilt_handlebarstemplate_test.rb +0 -276
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "simplecov"
4
+ require "simplecov-cobertura"
5
+
6
+ return if ENV["COVERAGE"] == "false" || ENV["APPRAISAL_INITIALIZED"]
7
+
8
+ SimpleCov.start do
9
+ add_filter "/vendor/"
10
+
11
+ coverage_dir "spec/reports/coverage"
12
+
13
+ formatter SimpleCov::Formatter::MultiFormatter.new([
14
+ SimpleCov::Formatter::CoberturaFormatter,
15
+ SimpleCov::Formatter::HTMLFormatter,
16
+ ])
17
+
18
+ minimum_coverage 100
19
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "spec_coverage"
4
+
5
+ require "handlebars/engine"
6
+ require_relative "fixtures/helpers"
7
+
8
+ RSpec.configure do |config|
9
+ # Enable flags like --only-failures and --next-failure
10
+ config.example_status_persistence_file_path = "spec/reports/status"
11
+
12
+ # Disable RSpec exposing methods globally on `Module` and `main`
13
+ config.disable_monkey_patching!
14
+
15
+ config.expect_with :rspec do |c|
16
+ c.syntax = :expect
17
+ end
18
+
19
+ config.add_formatter("documentation")
20
+ config.add_formatter("RspecJunitFormatter", "spec/reports/rspec.xml")
21
+
22
+ config.include(Fixtures::Helpers)
23
+ end
@@ -0,0 +1,543 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tilt/handlebars"
4
+
5
+ RSpec.describe Tilt::HandlebarsTemplate do
6
+ let(:render) { template.render(render_scope, render_locals, &render_block) }
7
+ let(:render_block) { nil }
8
+ let(:render_locals) { nil }
9
+ let(:render_scope) { nil }
10
+ let(:rendered) { "" }
11
+ let(:template) {
12
+ described_class.new(template_file, **template_options, &template_block)
13
+ }
14
+ let(:template_block) { nil }
15
+ let(:template_file) { nil }
16
+ let(:template_options) { {} }
17
+
18
+ describe "#initialize" do
19
+ subject { template }
20
+
21
+ let(:template_data) { fixture_data("views/hello.hbs") }
22
+ let(:rendered) { "Hello, .\n" }
23
+
24
+ context "with a block" do
25
+ let(:template_block) { ->(_template = nil) { template_data } }
26
+
27
+ it "reads from the block" do
28
+ expect(template.data).to eq(template_data)
29
+ end
30
+
31
+ describe "rendering" do
32
+ it "renders correctly" do
33
+ expect(render).to eq(rendered)
34
+ end
35
+ end
36
+ end
37
+
38
+ context "with a file" do
39
+ let(:template_file) { fixture_file("views/hello.hbs") }
40
+
41
+ it "reads from the file" do
42
+ expect(template.data).to eq(template_data)
43
+ end
44
+
45
+ describe "rendering" do
46
+ it "renders correctly" do
47
+ expect(render).to eq(rendered)
48
+ end
49
+ end
50
+ end
51
+
52
+ context "with a path" do
53
+ let(:template_file) { fixture_path("views/hello.hbs") }
54
+
55
+ it "reads from the file" do
56
+ expect(template.data).to eq(template_data)
57
+ end
58
+
59
+ describe "rendering" do
60
+ it "renders correctly" do
61
+ expect(render).to eq(rendered)
62
+ end
63
+ end
64
+ end
65
+
66
+ context "with nothing" do
67
+ it "raises an error" do
68
+ expect { render }.to raise_error(ArgumentError)
69
+ end
70
+ end
71
+ end
72
+
73
+ describe "#allows_script?" do
74
+ subject { template.allows_script? }
75
+
76
+ let(:template_file) { fixture_path("views/hello.hbs") }
77
+
78
+ it { is_expected.to eq(false) }
79
+ end
80
+
81
+ if Tilt::VERSION[/\d+/].to_i > 1
82
+ describe "#metadata" do
83
+ subject { template.metadata }
84
+
85
+ let(:template_file) { fixture_path("views/hello.hbs") }
86
+
87
+ it { is_expected.to include(allows_script: false) }
88
+ end
89
+ end
90
+
91
+ describe "#render" do
92
+ subject { render }
93
+
94
+ let(:render_scope) { { name: "World", type: "planet" } }
95
+ let(:rendered) { "Hello, World (a 'planet')." }
96
+ let(:template_block) { ->(_template = nil) { template_data } }
97
+ let(:template_data) { "Hello, {{name}} (a '{{type}}')." }
98
+
99
+ it "can be called multiple times" do
100
+ 3.times do
101
+ expect(render).to eq(rendered)
102
+ end
103
+ end
104
+
105
+ describe "parameters" do
106
+ describe "scope" do
107
+ let(:render_scope) { { name: "World", type: "planet" } }
108
+
109
+ it "is accessible in the context" do
110
+ expect(render).to eq(rendered)
111
+ end
112
+
113
+ shared_examples "with locals" do
114
+ context "with locals" do
115
+ let(:rendered) { "Hello, Mars (a 'planet')." }
116
+ let(:render_locals) { { name: "Mars", type: "planet" } }
117
+
118
+ it "is merged with the locals" do
119
+ expect(render).to eq(rendered)
120
+ end
121
+ end
122
+ end
123
+
124
+ include_examples "with locals"
125
+
126
+ context "with a nested value" do
127
+ let(:render_scope) { { person: { name: "World" } } }
128
+ let(:rendered) { "Hello, World." }
129
+ let(:template_data) { "Hello, {{person.name}}." }
130
+
131
+ it "is accessible in the context" do
132
+ expect(render).to eq(rendered)
133
+ end
134
+ end
135
+
136
+ context "with an object" do
137
+ let(:object_class) {
138
+ Class.new do
139
+ attr_accessor :name, :type
140
+
141
+ def initialize(name, type)
142
+ @name = name
143
+ @type = type
144
+ end
145
+ end
146
+ }
147
+ let(:render_scope) { object_class.new("World", "planet") }
148
+
149
+ context "when it responds to #to_h" do
150
+ let(:rendered) { "Hello, World (a '')." }
151
+
152
+ before do
153
+ def render_scope.to_h
154
+ { name: name }
155
+ end
156
+ end
157
+
158
+ it "merges the result into the context" do
159
+ expect(render).to eq(rendered)
160
+ end
161
+
162
+ include_examples "with locals"
163
+ end
164
+
165
+ context "when it does not respond to #to_h" do
166
+ let(:rendered) { "Hello, World (a 'planet')." }
167
+
168
+ it "merges the instance variables into the context" do
169
+ expect(render).to eq(rendered)
170
+ end
171
+
172
+ include_examples "with locals"
173
+ end
174
+ end
175
+ end
176
+
177
+ describe "locals" do
178
+ let(:render_scope) { nil }
179
+ let(:render_locals) { { name: "World", type: "planet" } }
180
+
181
+ it "is accessible in the render context" do
182
+ expect(render).to eq(rendered)
183
+ end
184
+
185
+ describe "a nested value" do
186
+ let(:render_locals) { { person: { name: "World" } } }
187
+ let(:rendered) { "Hello, World." }
188
+ let(:template_data) { "Hello, {{person.name}}." }
189
+
190
+ it "is accessible in the render context" do
191
+ expect(render).to eq(rendered)
192
+ end
193
+ end
194
+ end
195
+
196
+ describe "block" do
197
+ let(:render_block) { -> { "World (a '{{type}}')" } }
198
+ let(:rendered) { "Hello, World (a '{{type}}')." }
199
+ let(:template_data) { "Hello, {{{yield}}}." }
200
+
201
+ it "is accessible in the render context" do
202
+ expect(render).to eq(rendered)
203
+ end
204
+ end
205
+ end
206
+
207
+ describe "comments" do
208
+ context "when {{!...}}" do
209
+ let(:template_data) { "Hello, World (a 'planet').{{! Comment }}" }
210
+
211
+ it "is not rendered" do
212
+ expect(render).to eq(rendered)
213
+ end
214
+ end
215
+
216
+ context "when {{!--...--}}" do
217
+ let(:template_data) { "Hello, World (a 'planet').{{!-- Comment --}}" }
218
+
219
+ it "is not rendered" do
220
+ expect(render).to eq(rendered)
221
+ end
222
+ end
223
+ end
224
+
225
+ describe "helpers" do
226
+ describe "each" do
227
+ let(:template_data) {
228
+ <<~TEMPLATE.chomp
229
+ Hello, {{#each items~}}
230
+ {{~this}}{{#unless @last}}/{{/unless~}}
231
+ {{~else}}no one
232
+ {{~/each}}.
233
+ TEMPLATE
234
+ }
235
+
236
+ context "with items" do
237
+ let(:render_scope) { { items: ["Mars", "World"] } }
238
+ let(:rendered) { "Hello, Mars/World." }
239
+
240
+ it "renders correctly" do
241
+ expect(render).to eq(rendered)
242
+ end
243
+ end
244
+
245
+ context "when empty" do
246
+ let(:render_scope) { { items: [] } }
247
+ let(:rendered) { "Hello, no one." }
248
+
249
+ it "renders correctly" do
250
+ expect(render).to eq(rendered)
251
+ end
252
+ end
253
+ end
254
+
255
+ describe "if" do
256
+ let(:template_data) {
257
+ "Hello, {{#if test}}Then{{else}}Else{{/if}}."
258
+ }
259
+
260
+ context "when true" do
261
+ let(:render_scope) { { test: true } }
262
+ let(:rendered) { "Hello, Then." }
263
+
264
+ it "renders correctly" do
265
+ expect(render).to eq(rendered)
266
+ end
267
+ end
268
+
269
+ context "when false" do
270
+ let(:render_scope) { { test: false } }
271
+ let(:rendered) { "Hello, Else." }
272
+
273
+ it "renders correctly" do
274
+ expect(render).to eq(rendered)
275
+ end
276
+ end
277
+
278
+ context "when undefined" do
279
+ let(:render_scope) { {} }
280
+ let(:rendered) { "Hello, Else." }
281
+
282
+ it "renders correctly" do
283
+ expect(render).to eq(rendered)
284
+ end
285
+ end
286
+ end
287
+
288
+ describe "unless" do
289
+ let(:template_data) {
290
+ "Hello, {{#unless test}}Then{{else}}Else{{/unless}}."
291
+ }
292
+
293
+ context "when true" do
294
+ let(:render_scope) { { test: true } }
295
+ let(:rendered) { "Hello, Else." }
296
+
297
+ it "renders correctly" do
298
+ expect(render).to eq(rendered)
299
+ end
300
+ end
301
+
302
+ context "when false" do
303
+ let(:render_scope) { { test: false } }
304
+ let(:rendered) { "Hello, Then." }
305
+
306
+ it "renders correctly" do
307
+ expect(render).to eq(rendered)
308
+ end
309
+ end
310
+
311
+ context "when undefined" do
312
+ let(:render_scope) { {} }
313
+ let(:rendered) { "Hello, Then." }
314
+
315
+ it "renders correctly" do
316
+ expect(render).to eq(rendered)
317
+ end
318
+ end
319
+ end
320
+
321
+ describe "with" do
322
+ let(:render_scope) { { person: { name: "World" } } }
323
+ let(:rendered) { "Hello, World." }
324
+ let(:template_data) { "Hello, {{#with person}}{{name}}{{/with}}." }
325
+
326
+ it "renders correctly" do
327
+ expect(render).to eq(rendered)
328
+ end
329
+ end
330
+
331
+ describe "custom" do
332
+ describe "simple" do
333
+ let(:rendered) { "Hello, WORLD AND MARS." }
334
+
335
+ before do
336
+ template.register_helper(:upper) do |_ctx, arg|
337
+ arg.upcase
338
+ end
339
+ end
340
+
341
+ context "when variable" do
342
+ let(:template_data) { "Hello, {{upper name}} AND MARS." }
343
+
344
+ it "renders correctly" do
345
+ expect(render).to eq(rendered)
346
+ end
347
+ end
348
+
349
+ context "when quoted" do
350
+ let(:template_data) { "Hello, {{upper 'world and mars'}}." }
351
+
352
+ it "renders correctly" do
353
+ expect(render).to eq(rendered)
354
+ end
355
+ end
356
+ end
357
+
358
+ describe "block" do
359
+ let(:rendered) { "Hello, WORLD." }
360
+ let(:template_data) { "Hello, {{#upper}}{{name}}{{/upper}}." }
361
+
362
+ before do
363
+ template.register_helper(:upper, <<~JS)
364
+ function(options) {
365
+ return options.fn(this).toUpperCase();
366
+ }
367
+ JS
368
+ end
369
+
370
+ it "renders correctly" do
371
+ expect(render).to eq(rendered)
372
+ end
373
+ end
374
+ end
375
+ end
376
+
377
+ describe "HTML" do
378
+ context "when returned by {{...}}" do
379
+ let(:rendered) { "Hello, &amp;&lt;&gt;&quot;&#x27;&#x60;&#x3D;." }
380
+ let(:render_scope) { { name: "&<>\"'`=" } }
381
+ let(:template_data) { "Hello, {{name}}." }
382
+
383
+ it "escapes special characters" do
384
+ expect(render).to eq(rendered)
385
+ end
386
+ end
387
+
388
+ context "when returned by {{{...}}}" do
389
+ let(:rendered) { "Hello, &<>\"'`=." }
390
+ let(:render_scope) { { name: "&<>\"'`=" } }
391
+ let(:template_data) { "Hello, {{{name}}}." }
392
+
393
+ it "does not escape special characters" do
394
+ expect(render).to eq(rendered)
395
+ end
396
+ end
397
+ end
398
+
399
+ describe "partials" do
400
+ context "when data is from a file" do
401
+ let(:rendered) { "Hello, World (a 'planet')\n.\n" }
402
+ let(:template_block) { nil }
403
+
404
+ context "when file is missing" do
405
+ let(:template_file) { fixture_path("views/hello_missing.hbs") }
406
+
407
+ it "raises an error" do
408
+ expect { render }.to raise_error(Tilt::Handlebars::Error)
409
+ end
410
+ end
411
+
412
+ context "when file exists" do
413
+ let(:template_file) { fixture_path("views/hello_partial.hbs") }
414
+
415
+ it "renders correctly" do
416
+ expect(render).to eq(rendered)
417
+ end
418
+ end
419
+
420
+ context "when partial has extension `handlebars`" do
421
+ let(:template_file) { fixture_path("views/hello_partial2.hbs") }
422
+
423
+ it "renders correctly" do
424
+ expect(render).to eq(rendered)
425
+ end
426
+ end
427
+
428
+ context "when partial is registered" do
429
+ let(:rendered) { "Hello, World.\n" }
430
+ let(:template_file) { fixture_path("views/hello_partial.hbs") }
431
+
432
+ before do
433
+ template.register_partial(:partial, "{{name}}")
434
+ end
435
+
436
+ it "renders correctly" do
437
+ expect(render).to eq(rendered)
438
+ end
439
+ end
440
+ end
441
+
442
+ context "when data is from a string" do
443
+ let(:template_data) { "Hello, {{> partial}}." }
444
+
445
+ context "when partial is missing" do
446
+ it "raises an error" do
447
+ expect { render }.to raise_error(Tilt::Handlebars::Error)
448
+ end
449
+ end
450
+
451
+ context "when partial is registered" do
452
+ let(:rendered) { "Hello, World." }
453
+
454
+ before do
455
+ template.register_partial(:partial, "{{name}}")
456
+ end
457
+
458
+ it "renders correctly" do
459
+ expect(render).to eq(rendered)
460
+ end
461
+ end
462
+ end
463
+
464
+ describe "#load_partial" do
465
+ let(:rendered) { "Hello, World (a 'planet')\n.\n" }
466
+
467
+ context "when path is absolute" do
468
+ let(:rendered) { "Hello, World (a 'planet')\n." }
469
+ let(:template_data) {
470
+ "Hello, {{> '#{fixture_dir}/views/partial'}}."
471
+ }
472
+
473
+ it "renders correctly" do
474
+ expect(render).to eq(rendered)
475
+ end
476
+ end
477
+
478
+ context "when path is relative" do
479
+ let(:template_block) { nil }
480
+ let(:template_file) { fixture_path("views/hello_partial.hbs") }
481
+
482
+ it "renders correctly" do
483
+ expect(render).to eq(rendered)
484
+ end
485
+ end
486
+ end
487
+
488
+ describe "#partial_missing" do
489
+ let(:rendered) { "Hello, World (from partial)." }
490
+ let(:template_data) { "Hello, {{> partial}}." }
491
+
492
+ before do
493
+ template.partial_missing do |name|
494
+ "{{name}} (from #{name})"
495
+ end
496
+ end
497
+
498
+ it "renders correctly" do
499
+ expect(render).to eq(rendered)
500
+ end
501
+ end
502
+ end
503
+
504
+ describe "variables" do
505
+ describe "arrays" do
506
+ let(:render_scope) { { items: ["Mars", "World"] } }
507
+
508
+ describe "access" do
509
+ let(:rendered) { "Hello, World." }
510
+ let(:template_data) { "Hello, {{items.[1]}}." }
511
+
512
+ it "renders correctly" do
513
+ expect(render).to eq(rendered)
514
+ end
515
+ end
516
+ end
517
+ end
518
+ end
519
+
520
+ describe "#respond_to?" do
521
+ subject { template.respond_to?(name) }
522
+
523
+ let(:template_file) { fixture_path("views/hello.hbs") }
524
+
525
+ context "when name is `register_helper`" do
526
+ let(:name) { :register_helper }
527
+
528
+ it { is_expected.to eq(true) }
529
+ end
530
+
531
+ context "when name is `register_partial`" do
532
+ let(:name) { :register_partial }
533
+
534
+ it { is_expected.to eq(true) }
535
+ end
536
+
537
+ context "when name is `undefined`" do
538
+ let(:name) { :undefined }
539
+
540
+ it { is_expected.to eq(false) }
541
+ end
542
+ end
543
+ end
data/spec/tilt_spec.rb ADDED
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Tilt do
4
+ let(:template_class) { Tilt::HandlebarsTemplate }
5
+
6
+ describe "[]" do
7
+ subject { described_class[path] }
8
+
9
+ let(:path) { "test.#{extension}" }
10
+
11
+ context "when the file extension is `handlebars`" do
12
+ let(:extension) { :handlebars }
13
+
14
+ it { is_expected.to eq(template_class) }
15
+ end
16
+
17
+ context "when the file extension is `hbs`" do
18
+ let(:extension) { :hbs }
19
+
20
+ it { is_expected.to eq(template_class) }
21
+ end
22
+ end
23
+
24
+ describe "new" do
25
+ subject(:template) { described_class.new(template_file, **template_opts) }
26
+
27
+ let(:template_file) { fixture_path("views/hello.hbs") }
28
+ let(:template_opts) { { lazy: false, path: nil } }
29
+
30
+ it { is_expected.to be_a(template_class) }
31
+
32
+ it "sets the file" do
33
+ expect(template.file).to eq(template_file)
34
+ end
35
+
36
+ it "sets the options" do
37
+ expect(template.options).to eq(template_opts)
38
+ end
39
+ end
40
+ end
data/tasks/doc.rake ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yard"
4
+
5
+ YARD::Rake::YardocTask.new(:doc) do |t|
6
+ t.options = []
7
+ t.stats_options = ["--list-undoc"]
8
+ end
data/tasks/gem.rake ADDED
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Tasks:
4
+ # - build: Build gem into the pkg directory
5
+ # - install: Build and install gem into system gems
6
+ # - release: Create git tag and build and push gem
7
+ require "bundler/gem_tasks"
data/tasks/lint.rake ADDED
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rubocop/rake_task"
4
+
5
+ # Tasks:
6
+ # - lint: run rubocop
7
+ RuboCop::RakeTask.new(:lint)
data/tasks/test.rake ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rspec/core/rake_task"
4
+
5
+ desc "Run all tests"
6
+ task test: [:spec]
7
+
8
+ # Tasks:
9
+ # - spec: run rspec
10
+ RSpec::Core::RakeTask.new