tilt-handlebars 1.2.0 → 2.0.0

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 (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