luigi-template 0.5.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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: a2d52d440f44dd02a4bfef5a305bef2bae1a3c25e2cbe8874e42a34ac53ce862
4
+ data.tar.gz: cb991f02739dee48a8f752e68fc2975e936269422e7fd937115824f2335a43de
5
+ SHA512:
6
+ metadata.gz: 2f977f467899c187c3bf05fa76e9f8f8170bd7edeed8a57452e2ce448f3930456cd80fbee1425d6e5c68121034b0ec3dd0af2c6f6e63f463718e387ee0cb9fc8
7
+ data.tar.gz: b4595175ca66f28c6bfec2ab31d4d4441dae8877ed12b23e69b3422ffa7bd95c6f41a023a776ee1ee04f7176ce71241ca626d0ba0b254bc2782cca5d49e4a688
@@ -0,0 +1,75 @@
1
+ Luigi Template
2
+ ==============
3
+
4
+ Overview
5
+ --------
6
+ Luigi Template is a string templating library for JavaScript, Java,
7
+ Ruby, and PHP.
8
+
9
+ Here's an example:
10
+
11
+ *TODO*
12
+
13
+ Features:
14
+
15
+ *TODO*
16
+
17
+ Documentation
18
+ -------------
19
+ The API documentation is available online at the following URL:
20
+
21
+ https://pablotron.github.io/luigi-template/ruby/
22
+
23
+ You can generate the API documentation in the `docs/` directory via
24
+ [RDoc][], like so:
25
+
26
+ # generate API documentation in docs/ directory
27
+ rake docs
28
+
29
+ Tests
30
+ -----
31
+ You can run the [minitest][] test suite via [Rake][], like so:
32
+
33
+ # run the test suite
34
+ rake test
35
+
36
+ To generate a [JUnit][]-compatible XML report, install the
37
+ [minitest-junit][] gem and then do the following:
38
+
39
+ # run the test suite and generate a junit-compatible report.xml
40
+ rake test TESTOPTS=--junit
41
+
42
+ Author
43
+ ------
44
+ Paul Duncan ([pabs@pablotron.org][me])<br/>
45
+ https://pablotron.org/
46
+
47
+ License
48
+ -------
49
+ Copyright 2010-2018 Paul Duncan ([pabs@pablotron.org][me])
50
+
51
+ Permission is hereby granted, free of charge, to any person obtaining a
52
+ copy of this software and associated documentation files (the
53
+ "Software"), to deal in the Software without restriction, including
54
+ without limitation the rights to use, copy, modify, merge, publish,
55
+ distribute, sublicense, and/or sell copies of the Software, and to
56
+ permit persons to whom the Software is furnished to do so, subject to
57
+ the following conditions:
58
+
59
+ The above copyright notice and this permission notice shall be included
60
+ in all copies or substantial portions of the Software.
61
+
62
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
63
+ OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
64
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
65
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
66
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
67
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
68
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
69
+
70
+ [JUnit]: https://junit.org/
71
+ [me]: mailto:pabs@pablotron.org
72
+ [minitest]: https://github.com/seattlerb/minitest
73
+ [minitest-junit]: https://github.com/aespinosa/minitest-junit
74
+ [RDoc]: https://github.com/ruby/rdoc
75
+ [Rake]: https://github.com/ruby/rake
@@ -0,0 +1,16 @@
1
+ require 'rake/testtask'
2
+ require 'rdoc/task'
3
+
4
+ Rake::TestTask.new do |t|
5
+ t.libs << 'test'
6
+ end
7
+
8
+ RDoc::Task.new :docs do |t|
9
+ t.main = "lib/luigi-template.rb"
10
+ t.rdoc_files.include('lib/*.rb')
11
+ t.rdoc_dir = 'docs'
12
+ # t.options << "--all"
13
+ end
14
+
15
+ desc "Run tests"
16
+ task :default => :test
@@ -0,0 +1,914 @@
1
+ #
2
+ # = Luigi Template
3
+ #
4
+ # == Overview
5
+ #
6
+ # Luigi Template is a string template library.
7
+ #
8
+ # == Examples
9
+ #
10
+ # Simple usage example:
11
+ #
12
+ # # load luigi template
13
+ # require 'luigi-template'
14
+ #
15
+ # # create template
16
+ # tmpl = Luigi::Template.new('hello %{name}')
17
+ #
18
+ # # run template and print result
19
+ # puts tmpl.run({
20
+ # name: 'Paul'
21
+ # })
22
+ #
23
+ # # prints "hello Paul"
24
+ #
25
+ # You can also filter values in templates, using the pipe symbol:
26
+ #
27
+ # # create template that converts name to upper-case
28
+ # tmpl = Luigi::Template.new('hello %{name | uc}')
29
+ #
30
+ # # run template and print result
31
+ # puts tmpl.run({
32
+ # name: 'Paul'
33
+ # })
34
+ #
35
+ # # prints "hello PAUL"
36
+ #
37
+ # Filters can be chained:
38
+ #
39
+ # # create template that converts name to upper-case and then
40
+ # # strips leading and trailing whitespace
41
+ # tmpl = Luigi::Template.new('hello %{name | uc | trim}')
42
+ #
43
+ # # run template and print result
44
+ # puts tmpl.run({
45
+ # name: ' Paul '
46
+ # })
47
+ #
48
+ # # prints "hello PAUL"
49
+ #
50
+ # Filters can take arguments:
51
+ #
52
+ # # create template that converts name to lowercase and then
53
+ # # calculates the SHA-1 digest of the result
54
+ # tmpl = Luigi::Template.new('hello %{name | lc | hash sha1}')
55
+ #
56
+ # # run template and print result
57
+ # puts tmpl.run({
58
+ # name: 'Paul',
59
+ # })
60
+ #
61
+ # # prints "hello a027184a55211cd23e3f3094f1fdc728df5e0500"
62
+ #
63
+ # You can define custom global filters:
64
+ #
65
+ # # create custom global filter named 'foobarify'
66
+ # Luigi::FILTERS[:foobarify] = proc { |s| "foo-#{s}-bar" }
67
+ #
68
+ # # create template which uses custom "foobarify" filter
69
+ # tmpl = Luigi::Template.new('hello %{name | foobarify}')
70
+ #
71
+ # # run template and print result
72
+ # puts tmpl.run({
73
+ # name: 'Paul'
74
+ # })
75
+ #
76
+ # # prints "hello foo-Paul-bar"
77
+ #
78
+ # Or define custom filters for a template:
79
+ #
80
+ # # create template with custom filters rather than global filters
81
+ # tmpl = Luigi::Template.new('hello %{name | reverse}', {
82
+ # reverse: proc { |s| s.reverse }
83
+ # })
84
+ #
85
+ # # run template and print result
86
+ # puts tmpl.run({
87
+ # name: 'Paul',
88
+ # })
89
+ #
90
+ # # prints "hello luaP"
91
+ #
92
+ # Your custom filters can accept arguments, too:
93
+ #
94
+ # # create custom global filter named 'foobarify'
95
+ # Luigi::FILTERS[:wrap] = proc { |s, args|
96
+ # case args.length
97
+ # when 2
98
+ # '(%s, %s, %s)' % [args[0], s, args[1]]
99
+ # when 1
100
+ # '(%s in %s)' % [s, args[0]]
101
+ # when 0
102
+ # s
103
+ # else
104
+ # raise 'invalid argument count'
105
+ # end
106
+ # }
107
+ #
108
+ # # create template that uses custom "wrap" filter
109
+ # tmpl = Luigi::Template.new('sandwich: %{meat | wrap slice heel}, taco: %{meat | wrap shell}')
110
+ #
111
+ # # run template and print result
112
+ # puts tmpl.run({
113
+ # meat: 'chicken'
114
+ # })
115
+ #
116
+ # # prints "sandwich: (slice, chicken, heel), taco: (chicken in shell)"
117
+ #
118
+ # == Filters
119
+ #
120
+ # The following filters are built-in:
121
+ #
122
+ # * +uc+: Convert string to upper-case.
123
+ # * +lc+: Convert string to lower-case.
124
+ # * +h+: HTML-escape string.
125
+ # * +u+: URL-escape string.
126
+ # * +json+: JSON-encode value.
127
+ # * +trim+: Strip leading and trailing whitespace from string.
128
+ # * +base64+: Base64-encode value.
129
+ # * +hash+: Hash value. Requires hash algorithm parameter (ex:
130
+ # "sha1", "md5", etc).
131
+ #
132
+ # You can add your own global filters, like so:
133
+ #
134
+ # # create custom global filter named 'foobarify'
135
+ # Luigi::FILTERS[:foobarify] = proc { |s| "foo-#{s}-bar" }
136
+ #
137
+ # # create template which uses custom "foobarify" filter
138
+ # tmpl = Luigi::Template.new('hello %{name | foobarify}')
139
+ #
140
+ # # run template and print result
141
+ # puts tmpl.run({
142
+ # name: 'Paul'
143
+ # })
144
+ #
145
+ # # prints "hello foo-Paul-bar"
146
+ #
147
+ # == License
148
+ #
149
+ # Copyright 2007-2018 Paul Duncan ({pabs@pablotron.org}[mailto:pabs@pablotron.org])
150
+ #
151
+ # Permission is hereby granted, free of charge, to any person obtaining a
152
+ # copy of this software and associated documentation files (the
153
+ # "Software"), to deal in the Software without restriction, including
154
+ # without limitation the rights to use, copy, modify, merge, publish,
155
+ # distribute, sublicense, and/or sell copies of the Software, and to
156
+ # permit persons to whom the Software is furnished to do so, subject to
157
+ # the following conditions:
158
+ #
159
+ # The above copyright notice and this permission notice shall be included
160
+ # in all copies or substantial portions of the Software.
161
+ #
162
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
163
+ # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
164
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
165
+ # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
166
+ # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
167
+ # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
168
+ # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
169
+ #
170
+
171
+ require 'uri'
172
+ require 'json'
173
+ require 'openssl'
174
+ # require 'pp'
175
+
176
+ #
177
+ # Top-level Luigi namespace. See Luigi::Template for details.
178
+ #
179
+ # Example:
180
+ #
181
+ # # load luigi template
182
+ # require 'luigi-template'
183
+ #
184
+ # # create template
185
+ # tmpl = Luigi::Template.new('hello %{name}')
186
+ #
187
+ # # run template and print result
188
+ # puts tmpl.run({
189
+ # name: 'Paul'
190
+ # })
191
+ #
192
+ # # prints "hello Paul"
193
+ #
194
+ # You can also filter values in templates, using the pipe symbol:
195
+ #
196
+ # # create template that converts name to upper-case
197
+ # tmpl = Luigi::Template.new('hello %{name | uc}')
198
+ #
199
+ # # run template and print result
200
+ # puts tmpl.run({
201
+ # name: 'Paul'
202
+ # })
203
+ #
204
+ # # prints "hello PAUL"
205
+ #
206
+ # Filters can be chained:
207
+ #
208
+ # # create template that converts name to upper-case and then
209
+ # # strips leading and trailing whitespace
210
+ # tmpl = Luigi::Template.new('hello %{name | uc | trim}')
211
+ #
212
+ # # run template and print result
213
+ # puts tmpl.run({
214
+ # name: ' Paul '
215
+ # })
216
+ #
217
+ # # prints "hello PAUL"
218
+ #
219
+ # Filters can take arguments:
220
+ #
221
+ # # create template that converts name to lowercase and then
222
+ # # calculates the SHA-1 digest of the result
223
+ # tmpl = Luigi::Template.new('hello %{name | lc | hash sha1}')
224
+ #
225
+ # # run template and print result
226
+ # puts tmpl.run({
227
+ # name: 'Paul',
228
+ # })
229
+ #
230
+ # # prints "hello a027184a55211cd23e3f3094f1fdc728df5e0500"
231
+ #
232
+ # You can define custom global filters:
233
+ #
234
+ # # create custom global filter named 'foobarify'
235
+ # Luigi::FILTERS[:foobarify] = proc { |s| "foo-#{s}-bar" }
236
+ #
237
+ # # create template which uses custom "foobarify" filter
238
+ # tmpl = Luigi::Template.new('hello %{name | foobarify}')
239
+ #
240
+ # # run template and print result
241
+ # puts tmpl.run({
242
+ # name: 'Paul'
243
+ # })
244
+ #
245
+ # # prints "hello foo-Paul-bar"
246
+ #
247
+ # Or define custom filters for a template:
248
+ #
249
+ # # create template with custom filters rather than global filters
250
+ # tmpl = Luigi::Template.new('hello %{name | reverse}', {
251
+ # reverse: proc { |s| s.reverse }
252
+ # })
253
+ #
254
+ # # run template and print result
255
+ # puts tmpl.run({
256
+ # name: 'Paul',
257
+ # })
258
+ #
259
+ # # prints "hello luaP"
260
+ #
261
+ # Your custom filters can accept arguments, too:
262
+ #
263
+ # # create custom global filter named 'foobarify'
264
+ # Luigi::FILTERS[:wrap] = proc { |s, args|
265
+ # case args.length
266
+ # when 2
267
+ # '(%s, %s, %s)' % [args[0], s, args[1]]
268
+ # when 1
269
+ # '(%s in %s)' % [s, args[0]]
270
+ # when 0
271
+ # s
272
+ # else
273
+ # raise 'invalid argument count'
274
+ # end
275
+ # }
276
+ #
277
+ # # create template that uses custom "wrap" filter
278
+ # tmpl = Luigi::Template.new('sandwich: %{meat | wrap slice heel}, taco: %{meat | wrap shell}')
279
+ #
280
+ # # run template and print result
281
+ # puts tmpl.run({
282
+ # meat: 'chicken'
283
+ # })
284
+ #
285
+ # # prints "sandwich: (slice, chicken, heel), taco: (chicken in shell)"
286
+ #
287
+ module Luigi
288
+ #
289
+ # Version of Luigi Template.
290
+ #
291
+ VERSION = '0.5.0'
292
+
293
+ #
294
+ # Base class for all errors raised by Luigi Template.
295
+ #
296
+ class LuigiError < Exception
297
+ end
298
+
299
+ #
300
+ # Base class for unknown entry errors raised by Luigi Template.
301
+ #
302
+ class BaseUnknownError < LuigiError
303
+ #
304
+ # Type of unknown entry (Symbol).
305
+ #
306
+ attr_reader :type
307
+
308
+ #
309
+ # Name of unknown entry (String).
310
+ #
311
+ attr_reader :name
312
+
313
+ #
314
+ # Create a new BaseUnknownError instance.
315
+ #
316
+ # Parameters:
317
+ #
318
+ # * +type+: Type name (ex: "template", "filter", or "key").
319
+ # * +name+: Item name.
320
+ #
321
+ def initialize(type, name)
322
+ @type, @name = type, name
323
+ super("unknown #{type}: #{name}")
324
+ end
325
+ end
326
+
327
+ #
328
+ # Thrown by Luigi::Template#run when an unknown key is encountered.
329
+ #
330
+ # The key is available in the +name+ attribute.
331
+ #
332
+ class UnknownKeyError < BaseUnknownError
333
+ #
334
+ # Create a new UnknownFilterError instance.
335
+ #
336
+ # Parameters:
337
+ #
338
+ # * +name+: Unknown key.
339
+ #
340
+ def initialize(name)
341
+ super(:key, name)
342
+ end
343
+ end
344
+
345
+ #
346
+ # Thrown by Luigi::Template#run when an unknown filter is encountered.
347
+ #
348
+ # The unknown filter name is available in the +name+ attribute.
349
+ #
350
+ class UnknownFilterError < BaseUnknownError
351
+ #
352
+ # Create a new UnknownFilterError instance.
353
+ #
354
+ # Parameters:
355
+ #
356
+ # * +name+: Name of the unknown filter.
357
+ #
358
+ def initialize(name)
359
+ super(:filter, name)
360
+ end
361
+ end
362
+
363
+
364
+ #
365
+ # Thrown by Luigi::Cache#run when an unknown template is encountered.
366
+ #
367
+ # The unknown template name is available in the +name+ attribute.
368
+ #
369
+ class UnknownTemplateError < BaseUnknownError
370
+ #
371
+ # Create a new UnknownTemplateError instance.
372
+ #
373
+ # Parameters:
374
+ #
375
+ # * +name+: Unknown template name.
376
+ #
377
+ def initialize(name)
378
+ super(:template, name);
379
+ end
380
+ end
381
+
382
+ #
383
+ # HTML entity map.
384
+ #
385
+ # Used by built-in +h+ filter.
386
+ #
387
+ HTML_ENTITIES = {
388
+ 38 => '&amp;',
389
+ 60 => '&lt;',
390
+ 62 => '&gt;',
391
+ 34 => '&quot;',
392
+ 39 => '&apos;',
393
+ }
394
+
395
+ #
396
+ # Map of built-in global filters.
397
+ #
398
+ # Default Filters:
399
+ #
400
+ # * +uc+: Convert string to upper-case.
401
+ # * +lc+: Convert string to lower-case.
402
+ # * +h+: HTML-escape string.
403
+ # * +u+: URL-escape string.
404
+ # * +json+: JSON-encode value.
405
+ # * +trim+: Strip leading and trailing whitespace from string.
406
+ # * +base64+: Base64-encode value.
407
+ # * +hash+: Hash value. Requires hash algorithm parameter (ex:
408
+ # "sha1", "md5", etc).
409
+ #
410
+ # You can add your own global filters, like so:
411
+ #
412
+ # # create custom global filter named 'foobarify'
413
+ # Luigi::FILTERS[:foobarify] = proc { |s| "foo-#{s}-bar" }
414
+ #
415
+ # # create template which uses custom "foobarify" filter
416
+ # tmpl = Luigi::Template.new('hello %{name | foobarify}')
417
+ #
418
+ # # run template and print result
419
+ # puts tmpl.run({
420
+ # name: 'Paul'
421
+ # })
422
+ #
423
+ # # prints "hello foo-Paul-bar"
424
+ #
425
+ FILTERS = {
426
+ # upper-case string
427
+ uc: proc { |v|
428
+ (v || '').to_s.upcase
429
+ },
430
+
431
+ # lower-case string
432
+ lc: proc { |v|
433
+ (v || '').to_s.downcase
434
+ },
435
+
436
+ # html-escape string
437
+ h: proc { |v|
438
+ (v || '').to_s.bytes.map { |b|
439
+ if b < 32 || b > 126
440
+ "&##{b};"
441
+ elsif HTML_ENTITIES.key?(b)
442
+ HTML_ENTITIES[b]
443
+ else
444
+ b.chr
445
+ end
446
+ }.join
447
+ },
448
+
449
+ # uri-escape string
450
+ u: proc { |v|
451
+ URI.encode_www_form_component((v || '').to_s)
452
+ },
453
+
454
+ # json-encode value
455
+ json: proc { |v|
456
+ JSON.unparse(v)
457
+ },
458
+
459
+ # trim leading and trailing whitespace from string
460
+ trim: proc { |v, args, row, t|
461
+ (v || '').to_s.strip
462
+ },
463
+
464
+ # base64-encode string
465
+ base64: proc { |v, args, row, t|
466
+ [(v || '').to_s].pack('m').strip
467
+ },
468
+
469
+ # hash string
470
+ hash: proc { |v, args, row, t|
471
+ OpenSSL::Digest.new(args[0]).hexdigest((v || '').to_s)
472
+ },
473
+ }
474
+
475
+ #
476
+ # Template parser.
477
+ #
478
+ module Parser # :nodoc: all
479
+ RES = {
480
+ action: %r{
481
+ # match opening brace
482
+ %\{
483
+
484
+ # match optional whitespace
485
+ \s*
486
+
487
+ # match key
488
+ (?<key>[^\s\|\}]+)
489
+
490
+ # match filter(s)
491
+ (?<filters>(\s*\|(\s*[^\s\|\}]+)+)*)
492
+
493
+ # match optional whitespace
494
+ \s*
495
+
496
+ # match closing brace
497
+ \}
498
+
499
+ # or match up all non-% chars or a single % char
500
+ | (?<text>[^%]* | %)
501
+ }mx,
502
+
503
+ filter: %r{
504
+ # match filter name
505
+ (?<name>\S+)
506
+
507
+ # match filter arguments (optional)
508
+ (?<args>(\s*\S+)*)
509
+
510
+ # optional trailing whitespace
511
+ \s*
512
+ }mx,
513
+
514
+ delim_filters: %r{
515
+ \s*\|\s*
516
+ }mx,
517
+
518
+ delim_args: %r{
519
+ \s+
520
+ },
521
+ }.reduce({}) do |r, row|
522
+ r[row[0]] = row[1].freeze
523
+ r
524
+ end.freeze
525
+
526
+ #
527
+ # Parse a (possibly empty) string into an array of actions.
528
+ #
529
+ def self.parse_template(str)
530
+ str.scan(RES[:action]).map { |m|
531
+ if m[0] && m[0].length > 0
532
+ fs = parse_filters(m[1]).freeze
533
+ { type: :action, key: m[0].intern, filters: fs }
534
+ else
535
+ # literal text
536
+ { type: :text, text: m[2].freeze }
537
+ end.freeze
538
+ }.freeze
539
+ end
540
+
541
+ #
542
+ # Parse a (possibly empty) string into an array of filters.
543
+ #
544
+ def self.parse_filters(str)
545
+ # strip leading and trailing whitespace
546
+ str = (str || '').strip
547
+
548
+ if str.length > 0
549
+ str.strip.split(RES[:delim_filters]).inject([]) do |r, f|
550
+ # strip whitespace
551
+ f = f.strip
552
+
553
+ if f.length > 0
554
+ md = f.match(RES[:filter])
555
+ raise "invalid filter: #{f}" unless md
556
+ # pp md
557
+
558
+ # get args
559
+ args = md[:args].strip
560
+
561
+ # add to result
562
+ r << {
563
+ name: md[:name].intern,
564
+ args: args.length > 0 ? args.split(RES[:delim_args]) : [],
565
+ }
566
+ end
567
+
568
+ # return result
569
+ r
570
+ end
571
+ else
572
+ # return empty filter set
573
+ []
574
+ end
575
+ end
576
+ end
577
+
578
+ #
579
+ # Template class.
580
+ #
581
+ # Parse a template string into a Luigi::Template instance, and then
582
+ # apply the Luigi::Template via the Luigi::Template#run() method.
583
+ #
584
+ # Example:
585
+ #
586
+ # # load luigi template
587
+ # require 'luigi-template'
588
+ #
589
+ # # create template
590
+ # tmpl = Luigi::Template.new('hello %{name}')
591
+ #
592
+ # # run template and print result
593
+ # puts tmpl.run({
594
+ # name: 'Paul'
595
+ # })
596
+ #
597
+ # # prints "hello Paul"
598
+ #
599
+ # You can also filter values in templates, using the pipe symbol:
600
+ #
601
+ # # create template that converts name to upper-case
602
+ # tmpl = Luigi::Template.new('hello %{name | uc}')
603
+ #
604
+ # # run template and print result
605
+ # puts tmpl.run({
606
+ # name: 'Paul'
607
+ # })
608
+ #
609
+ # # prints "hello PAUL"
610
+ #
611
+ # Filters can be chained:
612
+ #
613
+ # # create template that converts name to upper-case and then
614
+ # # strips leading and trailing whitespace
615
+ # tmpl = Luigi::Template.new('hello %{name | uc | trim}')
616
+ #
617
+ # # run template and print result
618
+ # puts tmpl.run({
619
+ # name: ' Paul '
620
+ # })
621
+ #
622
+ # # prints "hello PAUL"
623
+ #
624
+ # Filters can take arguments:
625
+ #
626
+ # # create template that converts name to lowercase and then
627
+ # # calculates the SHA-1 digest of the result
628
+ # tmpl = Luigi::Template.new('hello %{name | lc | hash sha1}')
629
+ #
630
+ # # run template and print result
631
+ # puts tmpl.run({
632
+ # name: 'Paul',
633
+ # })
634
+ #
635
+ # # prints "hello a027184a55211cd23e3f3094f1fdc728df5e0500"
636
+ #
637
+ # You can define custom global filters:
638
+ #
639
+ # # create custom global filter named 'foobarify'
640
+ # Luigi::FILTERS[:foobarify] = proc { |s| "foo-#{s}-bar" }
641
+ #
642
+ # # create template which uses custom "foobarify" filter
643
+ # tmpl = Luigi::Template.new('hello %{name | foobarify}')
644
+ #
645
+ # # run template and print result
646
+ # puts tmpl.run({
647
+ # name: 'Paul'
648
+ # })
649
+ #
650
+ # # prints "hello foo-Paul-bar"
651
+ #
652
+ # Or define custom filters for a template:
653
+ #
654
+ # # create template with custom filters rather than global filters
655
+ # tmpl = Luigi::Template.new('hello %{name | reverse}', {
656
+ # reverse: proc { |s| s.reverse }
657
+ # })
658
+ #
659
+ # # run template and print result
660
+ # puts tmpl.run({
661
+ # name: 'Paul',
662
+ # })
663
+ #
664
+ # # prints "hello luaP"
665
+ #
666
+ # Your custom filters can accept arguments, too:
667
+ #
668
+ # # create custom global filter named 'foobarify'
669
+ # Luigi::FILTERS[:wrap] = proc { |s, args|
670
+ # case args.length
671
+ # when 2
672
+ # '(%s, %s, %s)' % [args[0], s, args[1]]
673
+ # when 1
674
+ # '(%s in %s)' % [s, args[0]]
675
+ # when 0
676
+ # s
677
+ # else
678
+ # raise 'invalid argument count'
679
+ # end
680
+ # }
681
+ #
682
+ # # create template that uses custom "wrap" filter
683
+ # tmpl = Luigi::Template.new('sandwich: %{meat | wrap slice heel}, taco: %{meat | wrap shell}')
684
+ #
685
+ # # run template and print result
686
+ # puts tmpl.run({
687
+ # meat: 'chicken'
688
+ # })
689
+ #
690
+ # # prints "sandwich: (slice, chicken, heel), taco: (chicken in shell)"
691
+ #
692
+ class Template
693
+ #
694
+ # Original template string.
695
+ #
696
+ attr_reader :str
697
+
698
+ #
699
+ # Create a new template, expand it with the given arguments and
700
+ # filters, and print the result.
701
+ #
702
+ # Parameters:
703
+ #
704
+ # * +str+: Template string.
705
+ # * +args+: Argument key/value map.
706
+ # * +filters+: Hash of filters. Defaults to Luigi::FILTERS if
707
+ # unspecified.
708
+ #
709
+ # Example:
710
+ #
711
+ # # create a template object, expand it, and print the result
712
+ # puts Luigi::Template.run('hello %{name}', {
713
+ # name: 'Paul'
714
+ # })
715
+ #
716
+ # # prints "hello Paul"
717
+ #
718
+ def self.run(str, args = {}, filters = FILTERS)
719
+ Template.new(str, filters).run(args)
720
+ end
721
+
722
+ #
723
+ # Create a new Template from the given string.
724
+ #
725
+ def initialize(str, filters = FILTERS)
726
+ @str, @filters = str.freeze, filters
727
+ @actions = Parser.parse_template(str).freeze
728
+ end
729
+
730
+ #
731
+ # Expand template with the given arguments and return the result.
732
+ #
733
+ # Parameters:
734
+ #
735
+ # * +args+: Argument key/value map.
736
+ #
737
+ # Example:
738
+ #
739
+ # # create a template object
740
+ # tmpl = Luigi::Template.new('hello %{name}')
741
+ #
742
+ # # apply template, print result
743
+ # puts tmpl.run({ name: 'Paul'})
744
+ #
745
+ # # prints "hello Paul"
746
+ #
747
+ # This method is aliased as "%", so you can do this:
748
+ #
749
+ # # create template
750
+ # tmpl = Luigi::Template.new('hello %{name | uc}')
751
+ #
752
+ # # run template and print result
753
+ # puts tmpl % { name: 'Paul' }
754
+ #
755
+ # # prints "hello PAUL"
756
+ #
757
+ def run(args)
758
+ @actions.map { |a|
759
+ # pp a
760
+
761
+ case a[:type]
762
+ when :action
763
+ # check key and get value
764
+ val = if args.key?(a[:key])
765
+ args[a[:key]]
766
+ elsif args.key?(a[:key].to_s)
767
+ args[a[:key].to_s]
768
+ else
769
+ # invalid key
770
+ raise UnknownKeyError.new(a[:key])
771
+ end
772
+
773
+ # filter value
774
+ a[:filters].inject(val) do |r, f|
775
+ # check filter name
776
+ unless @filters.key?(f[:name])
777
+ raise UnknownFilterError.new(f[:name])
778
+ end
779
+
780
+ # call filter, return result
781
+ @filters[f[:name]].call(r, f[:args], args, self)
782
+ end
783
+ when :text
784
+ # literal text
785
+ a[:text]
786
+ else
787
+ # never reached
788
+ raise "unknown action type: #{a[:type]}"
789
+ end
790
+ }.join
791
+ end
792
+
793
+ alias :'%' :run
794
+
795
+ #
796
+ # Return the input template string.
797
+ #
798
+ # Example:
799
+ #
800
+ # # create a template object
801
+ # tmpl = Luigi::Template.new('hello %{name}')
802
+ #
803
+ # # create a template object
804
+ # puts tmpl.to_s
805
+ #
806
+ # # prints "hello %{name}"
807
+ #
808
+ def to_s
809
+ @str
810
+ end
811
+ end
812
+
813
+ #
814
+ # Minimal lazy-loading template cache.
815
+ #
816
+ # Group a set of templates together and only parse them on an
817
+ # as-needed basis.
818
+ #
819
+ class Cache
820
+ #
821
+ # Create a new template cache with the given templates.
822
+ #
823
+ # Parameters:
824
+ #
825
+ # * +strings+: Map of template names to template strings.
826
+ # * +filters+: Hash of filters. Defaults to Luigi::FILTERS if
827
+ # unspecified.
828
+ #
829
+ # Example:
830
+ #
831
+ # # create template cache
832
+ # cache = Luigi::Cache.new({
833
+ # hi: 'hi %{name}!'
834
+ # })
835
+ #
836
+ # # run template from cache
837
+ # puts cache.run(:hi, {
838
+ # name: 'Paul'
839
+ # })
840
+ #
841
+ # # prints "hi paul!"
842
+ #
843
+ def initialize(strings, filters = FILTERS)
844
+ # work with frozen copy of strings hash
845
+ strings = strings.freeze
846
+
847
+ @templates = Hash.new do |h, k|
848
+ # always deal with symbols
849
+ k = k.intern
850
+
851
+ # make sure template exists
852
+ raise UnknownTemplateError.new(k) unless strings.key?(k)
853
+
854
+ # create template
855
+ h[k] = Template.new(strings[k], filters)
856
+ end
857
+ end
858
+
859
+ #
860
+ # Get given template, or raise an UnknownTemplateError if the given
861
+ # template does not exist.
862
+ #
863
+ # Example:
864
+ #
865
+ # # create template cache
866
+ # cache = Luigi::Cache.new({
867
+ # hi: 'hi %{name}!'
868
+ # })
869
+ #
870
+ # # get template from cache
871
+ # tmpl = cache[:hi]
872
+ #
873
+ # # run template, print result
874
+ # puts tmpl.run(:hi, {
875
+ # name: 'Paul'
876
+ # })
877
+ #
878
+ # # prints "hi Paul"
879
+ #
880
+ def [](key)
881
+ @templates[key]
882
+ end
883
+
884
+ #
885
+ # Run specified template from cache with the given templates.
886
+ #
887
+ # Raises an UnknownTemplateError if the given template key does not
888
+ # exist.
889
+ #
890
+ # Parameters:
891
+ #
892
+ # * +key+: Template key.
893
+ # * +args+: Argument key/value map.
894
+ #
895
+ # Example:
896
+ #
897
+ # # create template cache
898
+ # cache = Luigi::Cache.new({
899
+ # hi: 'hi %{name}!'
900
+ # })
901
+ #
902
+ # # run template from cache
903
+ # puts cache.run(:hi, {
904
+ # name: 'Paul'
905
+ # })
906
+ #
907
+ # # prints "hi paul!"
908
+ #
909
+ def run(key, args)
910
+ # run template with args and return result
911
+ @templates[key].run(args)
912
+ end
913
+ end
914
+ end
@@ -0,0 +1,28 @@
1
+ require 'minitest/autorun'
2
+ require 'luigi-template'
3
+
4
+ class CacheTest < MiniTest::Test
5
+ def test_cache
6
+ cache = Luigi::Cache.new({
7
+ foo: 'foo%{bar}',
8
+ })
9
+
10
+ r = cache.run(:foo, bar: 'foo')
11
+
12
+ assert_equal 'foofoo', r
13
+ end
14
+
15
+ def test_cache_with_custom_filters
16
+ cache = Luigi::Cache.new({
17
+ foo: 'foo%{bar | barify}',
18
+ }, {
19
+ barify: proc { |v|
20
+ "bar-#{v}-bar"
21
+ },
22
+ })
23
+
24
+ r = cache.run(:foo, bar: 'foo')
25
+
26
+ assert_equal 'foobar-foo-bar', r
27
+ end
28
+ end
@@ -0,0 +1,86 @@
1
+ require 'minitest/autorun'
2
+ require 'luigi-template'
3
+
4
+ class FiltersTest < MiniTest::Test
5
+ def test_uc
6
+ r = Luigi::Template.run('foo%{bar|uc}', {
7
+ bar: 'bar',
8
+ })
9
+
10
+ assert_equal 'fooBAR', r
11
+ end
12
+
13
+ def test_lc
14
+ r = Luigi::Template.run('foo%{bar|lc}', {
15
+ bar: 'BAR',
16
+ })
17
+
18
+ assert_equal 'foobar', r
19
+ end
20
+
21
+ def test_h
22
+ r = Luigi::Template.run('%{bar|h}', {
23
+ bar: "<>&\"'\x0f",
24
+ })
25
+
26
+ assert_equal '&lt;&gt;&amp;&quot;&apos;&#15;', r
27
+ end
28
+
29
+ def test_u
30
+ r = Luigi::Template.run('%{bar|u}', {
31
+ bar: "asdf<>&\"' \x0f",
32
+ })
33
+
34
+ assert_equal 'asdf%3C%3E%26%22%27+%0F', r
35
+ end
36
+
37
+ def test_json
38
+ want = '{"true":true,"false":false,"null":null,"number":5,"string":"foo","hash":{"foo":"bar"},"array":[0,1]}';
39
+
40
+ r = Luigi::Template.run('%{bar|json}', {
41
+ bar: {
42
+ true: true,
43
+ false: false,
44
+ null: nil,
45
+ number: 5,
46
+ string: 'foo',
47
+ hash: { foo: 'bar' },
48
+ array: [0, 1],
49
+ },
50
+ })
51
+
52
+ assert_equal want, r
53
+ end
54
+
55
+ def test_trim
56
+ r = Luigi::Template.run('foo%{bar|trim}', {
57
+ bar: "\r\n\t\v foo \r\n\t\v",
58
+ })
59
+
60
+ assert_equal 'foofoo', r
61
+ end
62
+
63
+ def test_base64
64
+ r = Luigi::Template.run('%{bar|base64}', {
65
+ bar: "foo",
66
+ })
67
+
68
+ assert_equal 'Zm9v', r
69
+ end
70
+
71
+ def test_hash_md5
72
+ r = Luigi::Template.run('%{bar|hash md5}', {
73
+ bar: "foo",
74
+ })
75
+
76
+ assert_equal 'acbd18db4cc2f85cedef654fccc4a4d8', r
77
+ end
78
+
79
+ def test_hash_sha1
80
+ r = Luigi::Template.run('%{bar|hash sha1}', {
81
+ bar: "foo",
82
+ })
83
+
84
+ assert_equal '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33', r
85
+ end
86
+ end
@@ -0,0 +1,32 @@
1
+ require 'minitest/autorun'
2
+ require 'luigi-template'
3
+
4
+ class ErrorsTest < MiniTest::Test
5
+ def test_unknown_key_error
6
+ assert_raises(Luigi::UnknownKeyError) do
7
+ Luigi::Template.run('foo%{unknown-key}', {
8
+ bar: 'foo',
9
+ })
10
+ end
11
+ end
12
+
13
+ def test_unknown_filter_error
14
+ assert_raises(Luigi::UnknownFilterError) do
15
+ Luigi::Template.run('foo%{bar | unknown-filter}', {
16
+ bar: 'foo',
17
+ })
18
+ end
19
+ end
20
+
21
+ def test_unknown_template_error
22
+ assert_raises(Luigi::UnknownTemplateError) do
23
+ cache = Luigi::Cache.new({
24
+ foo: 'foo%{bar}',
25
+ })
26
+
27
+ cache.run('unknown-template', {
28
+ bar: 'foo'
29
+ })
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,51 @@
1
+ require 'minitest/autorun'
2
+ require 'luigi-template'
3
+
4
+ class FiltersTest < MiniTest::Test
5
+ def test_filter
6
+ r = Luigi::Template.run('foo%{bar|h}', {
7
+ bar: '<',
8
+ })
9
+
10
+ assert_equal 'foo&lt;', r
11
+ end
12
+
13
+ def test_filter_chain
14
+ want = 'foofeab40e1fca77c7360ccca1481bb8ba5f919ce3a'
15
+ r = Luigi::Template.run('foo%{bar | uc | hash sha1}', {
16
+ bar: 'foo',
17
+ })
18
+
19
+ assert_equal want, r
20
+ end
21
+
22
+ def test_custom_global_filter
23
+ Luigi::FILTERS[:barify] = proc { |v| 'BAR' }
24
+
25
+ r = Luigi::Template.run('foo%{bar | barify}', {
26
+ bar: 'foo',
27
+ })
28
+
29
+ assert_equal 'fooBAR', r
30
+ end
31
+
32
+ def test_custom_template_filter
33
+ r = Luigi::Template.run('foo%{bar | barify}', {
34
+ bar: 'foo',
35
+ }, {
36
+ barify: proc { |v| 'BAR' }
37
+ })
38
+
39
+ assert_equal 'fooBAR', r
40
+ end
41
+
42
+ def test_filter_args
43
+ want = 'foo0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33'
44
+
45
+ r = Luigi::Template.run('foo%{bar | hash sha1}', {
46
+ bar: 'foo'
47
+ })
48
+
49
+ assert_equal want, r
50
+ end
51
+ end
@@ -0,0 +1,65 @@
1
+ require 'minitest/autorun'
2
+ require 'luigi-template'
3
+
4
+ class TemplateTest < MiniTest::Test
5
+ def test_new
6
+ t = Luigi::Template.new('foo%{bar}')
7
+ assert_instance_of Luigi::Template, t
8
+ end
9
+
10
+ def test_run
11
+ t = Luigi::Template.new('foo%{bar}')
12
+ r = t.run(bar: 'foo')
13
+
14
+ assert_equal 'foofoo', r
15
+ end
16
+
17
+ def test_run_with_string_key
18
+ t = Luigi::Template.new('foo%{bar}')
19
+ r = t.run('bar' => 'foo')
20
+
21
+ assert_equal 'foofoo', r
22
+ end
23
+
24
+ def test_self_run
25
+ r = Luigi::Template.run('foo%{bar}', bar: 'foo')
26
+ assert_equal 'foofoo', r
27
+ end
28
+
29
+ def test_multiple_keys
30
+ r = Luigi::Template.run('foo%{bar}%{baz}', {
31
+ bar: 'foo',
32
+ baz: 'bar',
33
+ })
34
+
35
+ assert_equal 'foofoobar', r
36
+ end
37
+
38
+ def test_whitespace
39
+ r = Luigi::Template.run('%{ bar}%{ bar }%{ bar}', {
40
+ bar: 'foo',
41
+ })
42
+
43
+ assert_equal 'foofoofoo', r
44
+ end
45
+
46
+ def test_newlines
47
+ r = Luigi::Template.run('%{
48
+ bar}%{
49
+ bar
50
+
51
+ }%{
52
+ bar}', {
53
+ bar: 'foo',
54
+ })
55
+
56
+ assert_equal 'foofoofoo', r
57
+ end
58
+
59
+ def test_to_s
60
+ want = '%{val | h}'
61
+ t = Luigi::Template.new(want)
62
+
63
+ assert_equal want, t.to_s
64
+ end
65
+ end
metadata ADDED
@@ -0,0 +1,51 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: luigi-template
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.5.0
5
+ platform: ruby
6
+ authors:
7
+ - Paul Duncan
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-09-05 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Simple string templating library.
14
+ email: pabs@pablotron.org
15
+ executables: []
16
+ extensions: []
17
+ extra_rdoc_files: []
18
+ files:
19
+ - README.mkd
20
+ - Rakefile
21
+ - lib/luigi-template.rb
22
+ - test/test_cache.rb
23
+ - test/test_default_filters.rb
24
+ - test/test_errors.rb
25
+ - test/test_filters.rb
26
+ - test/test_template.rb
27
+ homepage: https://github.com/pablotron/luigi-template
28
+ licenses:
29
+ - MIT
30
+ metadata: {}
31
+ post_install_message:
32
+ rdoc_options: []
33
+ require_paths:
34
+ - lib
35
+ required_ruby_version: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ required_rubygems_version: !ruby/object:Gem::Requirement
41
+ requirements:
42
+ - - ">="
43
+ - !ruby/object:Gem::Version
44
+ version: '0'
45
+ requirements: []
46
+ rubyforge_project:
47
+ rubygems_version: 2.7.6
48
+ signing_key:
49
+ specification_version: 4
50
+ summary: Simple string templating library.
51
+ test_files: []