luigi-template 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []