superfluid 0.0.0 → 0.2.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 46ef69a407c3fbeaa0e095f67b3cf42bf8862e4fc6d9a8327807619d6058bdcc
4
- data.tar.gz: fddb30cd353a521163f394fd9fc3b5b30235d19a3e48be6357d791d89a7ec13e
3
+ metadata.gz: fe8974f8f93054605035f4adfc394fe98414fcec3c9db66b58fd957fedbc0797
4
+ data.tar.gz: 95af81f7354548f0a908c429704a2338faafd60bd427d843b3495d045b89835b
5
5
  SHA512:
6
- metadata.gz: 900585c327e6a3539a6feb06be51b7d217b2b6bb8712ac7d436ff9d37ff774b12f4c6be89b9f4b7170b8f7b0779664fa43ea3e64b940419ec7a4620f19eafb55
7
- data.tar.gz: 0e398a2b3683039d6b247ea8b1fa33d75491b702d938534918d969ea86b1d8f1ce1daac4295d8de0ef5283228d7e3e3ff3771bac0f636a79d58d8fc39b2a23d1
6
+ metadata.gz: 1890b05c7e813598d6aed1877b2ce863a31d832a36ff7feef458afc82cada0c424209a6afbe02629ac167917a308c1ccbcfe7a1f28241c2bf9513fb4ff518a75
7
+ data.tar.gz: 85d24dff9714182d95c76edb935316945e24fc402ae203c77dced39b51b3be44782b774b2c868862bbaad5695b1405988064524c304de30c70e19fd9c9e7a9ee
checksums.yaml.gz.sig CHANGED
Binary file
data/README.adoc CHANGED
@@ -3,19 +3,25 @@
3
3
  :figure-caption!:
4
4
 
5
5
  :command_pattern_link: link:https://alchemists.io/articles/command_pattern[Command Pattern]
6
+ :containable_link: link:https://alchemists.io/projects/containable[Containable]
6
7
  :core_link: link:https://alchemists.io/projects/core[Core]
8
+ :functionable_link: link:https://alchemists.io/projects/functionable[Functionable]
7
9
  :liquid_link: link:https://shopify.github.io/liquid/[Liquid]
8
- :structs_link: link:https://alchemists.io/articles/ruby_structs[Structs]
10
+ :struct_link: link:https://alchemists.io/articles/ruby_structs[Struct]
9
11
 
10
12
  = Superfluid
11
13
 
14
+ Enhances {liquid_link} by smoothing out the rough edges with a strong focus on object composition and functional design. This includes a fully functional filter and tag registry so you can reuse lower level filters and tags within all aspects of your view layer.
15
+
12
16
  toc::[]
13
17
 
14
18
  == Features
15
19
 
16
- * Simple renderer initialization.
17
- * Powerful, and customizable, default environment with a cleaner Object API.
18
- * Ability to register filters as commands (see {command_pattern_link} for details).
20
+ * Uses a customizable default environment with a cleaner Object API.
21
+ * Uses {containable_link} for filter and tag registries.
22
+ * Registers filters via a `Module`, {functionable_link}, commands (see {command_pattern_link} for details), or containers (see {containable_link} for details).
23
+ * Registers tags via classes or containers (see {containable_link} for details).
24
+ * Provides a default renderer for quick rendering of templates and associated data.
19
25
 
20
26
  == Requirements
21
27
 
@@ -63,19 +69,48 @@ The quickest way to get started is to create a new instance:
63
69
  renderer = Superfluid.new
64
70
  ----
65
71
 
66
- Now you can render your template and associated data:
72
+ Then you can render your template and associated data:
67
73
 
68
74
  [source,ruby]
69
75
  ----
70
- renderer.call "Wow, that was {{ value }}!", "value" => "quick"
76
+ renderer.call "Hi, this is a {{ value }}.", "value" => "demo"
71
77
  ----
72
78
 
73
79
  The above will yield the following output:
74
80
 
75
81
  ----
76
- "Wow, that was quick!"
82
+ "Hi, this is a demo."
83
+ ----
84
+
85
+ The following sections detail how you can configure your environment, register filters, and register tags.
86
+
87
+ === Configuration
88
+
89
+ The configuration is used by the environment. This is created for you automatically but if you want an instance of the default configuration, use:
90
+
91
+ [source,ruby]
92
+ ----
93
+ configuration = Superfluid::Configuration.new
94
+
95
+ #<Struct:Superfluid::Configuration:0x00001780
96
+ default_resource_limits = {},
97
+ error_mode = :strict,
98
+ exception_renderer = #<Proc:0x0000000131200ee0 (lambda)>,
99
+ file_system = #<data Superfluid::Systems::Memory:0x000017b0 templates = {}>,
100
+ filter_registry = #<Superfluid::Registries::Filter:0x000000012f5a1290 @container=Superfluid::Filters::Container, @mode=:strict>,
101
+ tag_registry = #<Superfluid::Registries::Tag:0x000000012f5860d0 @container=Superfluid::Tags::Container>
102
+ >
77
103
  ----
78
104
 
105
+ As you can see, the configuration is a {struct_link} which means you can customize using any of these attributes:
106
+
107
+ * `default_resource_limits`: Limits the resources that a template can consume. Default: `{}`.
108
+ * `error_mode`: The error mode (link:https://github.com/Shopify/liquid#error-modes[details]). Default: `:strict`.
109
+ * `exception_renderer`: The function used to render error messages. Default: `Core::Identity` (see {core_link}).
110
+ * `file_system`: The file system. Default: `Superfluid::Systems::Memory.new`.
111
+ * `filter_registry`: The filter registry. Default: `Superfluid::Registries::Filter.new`.
112
+ * `tag_registry`: The tag registry. Default: `Superfluid::Registries::Tag.new`.
113
+
79
114
  === Environments
80
115
 
81
116
  To build the default environment, use:
@@ -85,100 +120,547 @@ To build the default environment, use:
85
120
  environment = Superfluid.build
86
121
  ----
87
122
 
88
- You can also build the default environment directly (which is what the above does:
123
+ To build the default environment directly (which is what the above does), use:
89
124
 
90
125
  [source,ruby]
91
126
  ----
92
- environment = Superfluid::Environment.for
127
+ environment = Superfluid::Environment.new
93
128
  ----
94
129
 
95
- Both of the above produce a _frozen_ environment (including frozen tags). If you need an environment that isn't frozen (tags included), use:
130
+ If you'd like to build a custom environment, you can do this several ways. For demonstration purposes, only the `error_mode` is used below but all configuration attributes are accepted:
96
131
 
97
132
  [source,ruby]
98
133
  ----
99
- environment = Superfluid::Environment.new
134
+ environment = Superfluid.build error_mode: :lax
135
+ environment = Superfluid::Environment.for error_mode: :lax
136
+ environment = Superfluid::Environment.new Superfluid::Configuration[error_mode: :lax]
100
137
  ----
101
138
 
102
- You can, later, freeze the environment once you've registered any/all filters and tags as follows:
139
+ You can also freeze the environment -- which also freezes the filter and tag registries -- once you've registered any/all filters and tags as follows:
103
140
 
104
141
  [source,ruby]
105
142
  ----
106
143
  environment.freeze
107
144
  ----
108
145
 
109
- Finally, the environment is an instance of a `Struct` which means you can leverage all of the power of {structs_link}.
146
+ === Systems
110
147
 
111
- When using `Superfluid.build`, `Superfluid::Environment.for`, or `Superfluid::Environment.new`, you can customize behavior via the following keyword arguments:
148
+ These are the file systems used to obtain templates for use within your custom tags. By default, an in memory system is provided for you: `Superfluid::Systems::Memory`. {liquid_link} also provides it's own systems as well. Each are described below.
112
149
 
113
- * `default_resource_limits`: The resource limits you want to set. Default: `{}`.
114
- * `error_mode`: The error mode. Default: `:strict`.
115
- * `exception_renderer`: The exception renderer. Default: `Core::Identity` (see {core_link} for details).
116
- * `file_system`: The file system. Default: `Superfluid::Systems::Memory.new`.
117
- * `filter_registry`: The filter registry. Default: `Superfluid::Registries::Filter.new`.
118
- * `tags`: The default tags. Default: `Liquid::Tags::STANDARD_TAGS`.
150
+ ==== Memory
151
+
152
+ Provided by this gem. Use to access templates via an in memory file system. This is enabled for you be default when using `Superfluid.new` and works in conjunction with the `template` tag which is also provided by this gem (see below for details). Again, this system is provided for you but you can be explicit if desired:
153
+
154
+ [source,ruby]
155
+ ----
156
+ environment = Superfluid.build file_system: Superfluid::Systems::Memory.new
157
+ renderer = Superfluid.new(environment:)
158
+
159
+ template = <<~CONTENT
160
+ {% template demo %}
161
+ Hi, this is a {{ subject }}.
162
+ {% endtemplate %}
163
+
164
+ {% render "demo", subject: "demo" %}
165
+ CONTENT
166
+
167
+ renderer.call template
168
+ # "Hi, this is a demo."
169
+ ----
170
+
171
+ ==== Local
172
+
173
+ Provided by {liquid_link}. Use to access templates via your local file system within a specific root folder. Usage:
174
+
175
+ [source,ruby]
176
+ ----
177
+ Pathname("templates").tap(&:mkdir).join("_demo.liquid").tap { it.write "{{ salutation }}" }
178
+
179
+ environment = Superfluid.build file_system: Liquid::LocalFileSystem.new(Pathname("templates"))
180
+ renderer = Superfluid.new(environment:)
181
+
182
+ renderer.call(%({% include "demo" %}, this is a demo), {"salutation" => "Hi"})
183
+ # "Hi, this is a demo"
184
+ ----
185
+
186
+ The above configures the local system to look for partials (templates) within the `templates` folder. You then reference them via the `include` tag.
187
+
188
+ By default, this system uses link:https://docs.ruby-lang.org/en/master/Kernel.html#method-i-format[Kernel#format] syntax to find your partials (i.e. `"_%s.liquid"`). If you don't like this pattern, you can configure differently when creating a new instance. Example:
189
+
190
+ [source,ruby]
191
+ ----
192
+ Liquid::LocalFileSystem.new "/a/path", "%s.html"
193
+ ----
194
+
195
+ ==== Blank
196
+
197
+ Provided by {liquid_link}. This is an abstract class -- meant for subclassing -- because it will fail with a `FileSystemError` when `read_template_file` isn't implemented. Usage:
198
+
199
+ [source,ruby]
200
+ ----
201
+ environment = Superfluid.build file_system: Liquid::BlankFileSystem.new(Pathname("templates"))
202
+ renderer = Superfluid.new(environment:)
203
+ ----
204
+
205
+ The above does nothing and is benign.
119
206
 
120
207
  === Filters
121
208
 
122
- By default, {liquid_link} only allows registration of filters as instance methods on a namespace (`Module`). Unfortunately, this leads to terrible design, bad practices, and unnecessarily hard to test objects. You can still register namespaces but this gem encourages the use of commands (see {command_pattern_link}) which allows you tap into Functional Programming by using procs, lambdas, and any object that responds to `#call`. This allow you to build -- and reuse -- more powerful filters.
209
+ By default, {liquid_link} only allows registration of filters as instance methods on a namespace (`Module`). Unfortunately, this leads to terrible design, bad practices, and unnecessarily hard to test objects. You can still register namespaces but this gem encourages the use of commands (see {command_pattern_link} for further details) and containers via {containable_link} which allows you tap into Functional Programming by using procs, lambdas, and any object that responds to `#call`. This allows you to build -- and reuse -- more powerful filters. The following details the different kinds of filters you can use, each broken down by category.
210
+
211
+ ==== Defaults
212
+
213
+ Default filters are provided for you automatically and can be viewed via your environment. Example:
214
+
215
+ [source,ruby]
216
+ ----
217
+ environment = Superfluid.build
218
+ environment.filter_registry.names
219
+ ----
220
+
221
+ The above will produce a list of all registered filters. These including all filters provided by {liquid_link} and those unique to this gem which are:
222
+
223
+ ===== `jsonify`
224
+
225
+ Renders objects as JSON. Example:
226
+
227
+ [source,ruby]
228
+ ----
229
+ renderer.call(%({{ data | jsonify }}), {"data" => {"a" => 1}})
230
+ # "{\"a\":1}"
231
+ ----
232
+
233
+ ===== `parse_json`
234
+
235
+ Parses JSON as a primitive. Example:
236
+
237
+ [source,ruby]
238
+ ----
239
+ renderer.call(
240
+ "{% assign data = payload | parse_json %}{{ data.one }}",
241
+ {"payload" => %({"one": 1, "two": 2})}
242
+ )
243
+
244
+ # "1"
245
+ ----
246
+
247
+ ===== `pluralize`
248
+
249
+ Renders a plural string. Example:
250
+
251
+ [source,ruby]
252
+ ----
253
+ renderer.call(%({{ text | pluralize: 0 }}), {"text" => "apple"})
254
+ # "0 apples"
255
+
256
+ renderer.call(%({{ text | pluralize: 1 }}), {"text" => "apple"})
257
+ # "1 apple"
258
+
259
+ renderer.call(%({{ text | pluralize: 2 }}), {"text" => "apple"})
260
+ # "2 apples"
261
+
262
+ renderer.call(%({{ text | pluralize: 3, "i", "us" }}), {"text" => "octopus"})
263
+ # "3 octopi"
264
+ ----
265
+
266
+ ===== `singularize`
267
+
268
+ Renders a singular string. Example:
269
+
270
+ [source,ruby]
271
+ ----
272
+ renderer.call(%({{ text | singularize: 0 }}), {"text" => "apples"})
273
+ # "0 apples"
274
+
275
+ renderer.call(%({{ text | singularize: 1 }}), {"text" => "apples"})
276
+ # "1 apple"
277
+
278
+ renderer.call(%({{ text | singularize: 2 }}), {"text" => "apples"})
279
+ # "2 apples"
280
+
281
+ renderer.call(%({{ text | singularize: 1, "i", "us" }}), {"text" => "octopi"})
282
+ # "1 octopus"
283
+ ----
284
+
285
+ ===== `trim_end`
286
+
287
+ Renders string with trimmed end. Example:
123
288
 
124
289
  [source,ruby]
125
290
  ----
126
- primary = Module.new { def one = 1 }
291
+ renderer.call(%({{ text | trim_end: 10 }}), {"text" => "A demo."})
292
+ # "A demo."
127
293
 
128
- secondary = Module.new do
129
- def two = 2
294
+ renderer.call(%({{ text | trim_end: 10 }}), {"text" => "This is a demo."})
295
+ # "This is..."
296
+
297
+ renderer.call(%({{ text | trim_end: 10, "", "---" }}), {"text" => "This is a demo."})
298
+ # "This is---"
299
+
300
+ renderer.call(%({{ text | trim_end: 10, "", "" }}), {"text" => "This is a demo."})
301
+ # "This is a"
302
+ ----
303
+
304
+ ===== `trim_middle`
305
+
306
+ Renders string with trimmed middle. Example:
307
+
308
+ [source,ruby]
309
+ ----
310
+ renderer.call(%({{ text | trim_middle: 20 }}), {"text" => "This is a demo."})
311
+ # "A demo."
130
312
 
131
- def three = 3
313
+ renderer.call(%({{ text | trim_middle: 13 }}), {"text" => "This is a demo."})
314
+ # "This...demo."
315
+
316
+ renderer.call(%({{ text | trim_middle: 13, "--" }}), {"text" => "This is a demo."})
317
+ # "This--demo."
318
+ ----
319
+
320
+ ==== Modules
321
+
322
+ Module instance methods are what {liquid_link} supports by default. You only need to define instance methods within a module and then register your module. Example:
323
+
324
+ [source,ruby]
325
+ ----
326
+ # Namespaces
327
+ module Primary
328
+ def echo(text) = text
329
+ end
330
+
331
+ module Secondary
332
+ def capitalize(text) = text.capitalize
333
+
334
+ def suffix(text, count = 1) = "#{text}-#{count}"
132
335
  end
133
336
 
134
- function_a = proc { "four" }
135
- function_b = proc { "five" }
337
+ # Renderer
338
+ renderer = Superfluid.new { it.register_filters Primary, Secondary }
339
+
340
+ # Results
341
+ renderer.call %({{ "demo" | echo }}) # "demo"
342
+ renderer.call %({{ "demo" | capitalize }}) # "Demo"
343
+ renderer.call %({{ "demo" | suffix }}) # "demo-1"
344
+ renderer.call %({{ "demo" | suffix: 2 }}) # "demo-2"
345
+ ----
346
+
347
+ As you can see, all three methods via the two namespaces are properly registered and immediately available for use. You'll also notice multiple namespaces were registered at once but you can register them individually and/or chain them. Examples:
348
+
349
+ [source,ruby]
350
+ ----
351
+ Superfluid.new do |environment|
352
+ # Single.
353
+ environment.register_filter Primary
136
354
 
137
- class Sayer
138
- def initialize prefix = "Demo"
139
- @prefix = prefix
355
+ # Multiple.
356
+ environment.register_filters Primary, Secondary
357
+
358
+ # Chained.
359
+ environment.register_filter(Primary)
360
+ .register_filter(Secondary)
361
+ .register_filters(Primary, Secondary)
362
+ end
363
+ ----
364
+
365
+ ==== Functionables
366
+
367
+ {functionable_link} modules are identical to the module examples, as shown above, except you require and extend instead. Example:
368
+
369
+ [source,ruby]
370
+ ----
371
+ require "functionable"
372
+
373
+ # Namespaces
374
+ module Primary
375
+ extend Functionable
376
+
377
+ def echo(text) = text
378
+ end
379
+
380
+ module Secondary
381
+ extend Functionable
382
+
383
+ def capitalize(text) = text.capitalize
384
+
385
+ def suffix(text, count = 1) = "#{text}-#{count}"
386
+ end
387
+ ----
388
+
389
+ This allows you to use purely functionable methods (see {functionable_link} documentation for details). At this point, you can register your functionable namespaces as follows:
390
+
391
+ [source,ruby]
392
+ ----
393
+ # Renderer
394
+ renderer = Superfluid.new { it.register_filters Primary, Secondary }
395
+
396
+ # Results
397
+ renderer.call %({{ "demo" | echo }}) # "demo"
398
+ renderer.call %({{ "demo" | capitalize }}) # "Demo"
399
+ renderer.call %({{ "demo" | suffix }}) # "demo-1"
400
+ renderer.call %({{ "demo" | suffix: 2 }}) # "demo-2"
401
+ ----
402
+
403
+ Registration is identical to modules:
404
+
405
+ [source,ruby]
406
+ ----
407
+ Superfluid.new do |environment|
408
+ # Single.
409
+ environment.register_filter Primary
410
+
411
+ # Multiple.
412
+ environment.register_filters Primary, Secondary
413
+
414
+ # Chained.
415
+ environment.register_filter(Primary)
416
+ .register_filter(Secondary)
417
+ .register_filters(Primary, Secondary)
418
+ end
419
+ ----
420
+
421
+ ==== Procs and Lambdas
422
+
423
+ Procs and lambdas are not supported by {liquid_link} but are via this gem. Example:
424
+
425
+ [source,ruby]
426
+ ----
427
+ # Setup
428
+
429
+ echo = proc { it }
430
+ capitalize = -> text { text.capitalize }
431
+ suffix = -> text, count = 1 { "#{text}-#{count}" }
432
+
433
+ # Renderer
434
+ renderer = Superfluid.new { it.register_filters echo:, capitalize:, suffix: }
435
+
436
+ # Results
437
+
438
+ renderer.call %({{ "demo" | echo }}) # "demo"
439
+ renderer.call %({{ "demo" | capitalize }}) # "Demo"
440
+ renderer.call %({{ "demo" | suffix }}) # "demo-1"
441
+ renderer.call %({{ "demo" | suffix: 2 }}) # "demo-2"
442
+ ----
443
+
444
+ Registration can be singular, plural, or chained:
445
+
446
+ [source,ruby]
447
+ ----
448
+ renderer = Superfluid.new do |environment|
449
+ # Single.
450
+ environment.register_filter(echo:)
451
+
452
+ # Multiple.
453
+ environment.register_filters(echo:, capitalize:, suffix:)
454
+
455
+ # Chained.
456
+ environment.register_filter(echo:)
457
+ .register_filter(capitalize:)
458
+ .register_filters(echo:, capitalize:, suffix:)
459
+ end
460
+ ----
461
+
462
+ ==== Classes
463
+
464
+ Classes, especially composable classes, are not supported by {liquid_link} but are via this gem. You only need to ensure you include `Core::Composable` and implement the `#call` method. Example:
465
+
466
+ [source,ruby]
467
+ ----
468
+ require "core"
469
+
470
+ class Suffixer
471
+ include Core::Composable
472
+
473
+ def initialize default: 1
474
+ @default = default
140
475
  end
141
476
 
142
- def call(message) = "#{prefix}: #{message}"
477
+ def call(text, suffix = default) = "#{text}-#{suffix}"
143
478
 
144
479
  private
145
480
 
146
- attr_reader :prefix
481
+ attr_reader :default
147
482
  end
483
+ ----
484
+
485
+ Now you can register and render with the above class as follows:
486
+
487
+ [source,ruby]
488
+ ----
489
+ # Renderer
490
+ renderer = Superfluid.new { it.register_filters suffix: Suffixer.new }
491
+
492
+ # Results
493
+
494
+ renderer.call %({{ "demo" | suffix }}) # "demo-1"
495
+ renderer.call %({{ "demo" | suffix: 2 }}) # "demo-2"
496
+ ----
497
+
498
+ Registration can be singular, plural, or chained:
499
+
500
+ [source,ruby]
501
+ ----
502
+ suffix = Suffixer.new
503
+
504
+ renderer = Superfluid.new do |environment|
505
+ # Single.
506
+ environment.register_filter(suffix:)
507
+
508
+ # Multiple.
509
+ environment.register_filters(suffix:)
510
+
511
+ # Chained.
512
+ environment.register_filter(suffix:)
513
+ .register_filters(suffix:)
514
+ end
515
+ ----
516
+
517
+ ==== Containers
518
+
519
+ Containers are not supported by {liquid_link} but are via this gem using {containable_link}. To use, create a container and then register your dependencies:
520
+
521
+ [source,ruby]
522
+ ----
523
+ require "containable"
524
+
525
+ module Container
526
+ extend Containable
148
527
 
149
- environment = Superfluid.build do |instance|
150
- instance.register_filter(primary)
151
- .register_filter(primary, four: function_a, sayer: Sayer.new)
152
- .register_filters(primary, secondary, four: function_a, five: function_b)
528
+ register(:echo) { |text| text }
529
+ register(:suffix) { |text, count = 1| "#{text}-#{count}" }
153
530
  end
154
531
  ----
155
532
 
156
- While the above is contrived, duplicate keys are ignored. Use of `#register_filter` is for backwards compatibility with {liquid_link}. All you need is `#register_filters` to register both namespaces (modules) and commands.
533
+ Now you can merge the container as follows:
534
+
535
+ [source,ruby]
536
+ ----
537
+ renderer = Superfluid.new { it.merge_filters Container }
538
+ ----
539
+
540
+ You can also selectively merge by supplying only the filters you care about from the container:
541
+
542
+ [source,ruby]
543
+ ----
544
+ renderer = Superfluid.new { it.merge_filters Container, :suffix }
545
+ ----
546
+
547
+ Once registered, then you can use as follows:
548
+
549
+ [source,ruby]
550
+ ----
551
+ renderer.call %({{ "demo" | echo }}) # "demo"
552
+ renderer.call %({{ "demo" | suffix }}) # "demo-1"
553
+ renderer.call %({{ "demo" | suffix: 2 }}) # "demo-2"
554
+ ----
555
+
556
+ With containers, you have the full capabilities of the {containable_link}. The above only scratches the surface of what's possible.
157
557
 
158
558
  === Tags
159
559
 
160
- As per {liquid_link} documentation, tags only need to inherit from `Liquid::Tag` and be initialized with `tag_name`, `factor`, and `tokens`. Then you can implement a render method which accepts a `context`. Example:
560
+ As per {liquid_link} documentation, tags only need to inherit from `Liquid::Tag` and be initialized with `tag_name`, `factor`, and `tokens` parameters. Then you can implement a render method which accepts a `context`. Example:
161
561
 
162
562
  [source,ruby]
163
563
  ----
164
- class Multiply < Liquid::Tag
165
- def initialize(tag_name, factor, tokens)
166
- super
167
- @factor = factor.to_f
168
- end
564
+ # Tag
565
+ class Sample < Liquid::Block
566
+ def render(context) = super.sub("<placeholder>", rand(100).to_s)
567
+ end
169
568
 
170
- def render(context) = (factor * context["multiply_by"].to_f).to_s
569
+ # Renderer
570
+ renderer = Superfluid.new { it.register_tag :sample, Sample }
171
571
 
172
- private
572
+ # Results
573
+ renderer.call "{% sample %}Your value is: <placeholder>.{% endsample %}"
574
+ # "Your value is: 80."
575
+ ----
576
+
577
+ You can also register single or multiple tags at once:
578
+
579
+ [source,ruby]
580
+ ----
581
+ renderer = Superfluid.new do |environment|
582
+ # Single.
583
+ environment.register_tag(:sample, Sample)
584
+
585
+ # Multiple.
586
+ environment.register_tags(one: Sample, two: Sample)
173
587
 
174
- attr_reader :factor
588
+ # Chained.
589
+ environment.register_tag(:sample, Sample)
590
+ .register_tags(one: Sample, two: Sample)
175
591
  end
592
+ ----
593
+
594
+ ==== Defaults
595
+
596
+ You have access to all tags supported by {liquid_link} including the tags provided by this gem (which is only the `template` tag at the moment).
597
+
598
+ ===== `template`
599
+
600
+ This tag must be used with the `Memory` system (see _Systems_, mentioned earlier, for details). You can use this tag as follows:
601
+
602
+ [source,ruby]
603
+ ----
604
+ renderer = Superfluid.new
605
+
606
+ template = <<~CONTENT
607
+ {% template demo %}
608
+ Hi, this is a {{ subject }}.
609
+ {% endtemplate %}
610
+
611
+ {% render "demo", subject: "demo" %}
612
+ CONTENT
613
+
614
+ renderer.call template
615
+ # "Hi, this is a demo."
616
+ ----
617
+
618
+ As you can see this tag requires two steps:
619
+
620
+ . Define by using the `template` tag followed by a unique name for your template (in this case: `demo`). You can then use any filters and/or additional tags with your template. Finally, close the tag with `endtemplate`.
621
+ . Render your template by using `render` followed by the unique name of your template (i.e. `demo`) and any attributes you need to provide to the template.
176
622
 
177
- environment = Superfluid.build do |instance|
178
- instance.register_tag(:multiply, Multiply)
623
+ The above allows you to define multiple templates for rendering later. Great for situations where you want to organize your in memory code for final later rendering.
624
+
625
+ ==== Containers
626
+
627
+ Containers are not supported by {liquid_link} but are via this gem using {containable_link}. To use, create a container and then register your dependencies:
628
+
629
+ [source,ruby]
630
+ ----
631
+ require "containable"
632
+
633
+ module Container
634
+ extend Containable
635
+
636
+ register :sample, Sample
179
637
  end
180
638
  ----
181
639
 
640
+ Now you can merge the container as follows:
641
+
642
+ [source,ruby]
643
+ ----
644
+ renderer = Superfluid.new { it.merge_tags Container }
645
+ ----
646
+
647
+ You can also selectively merge by supplying only the tags you care about from the container:
648
+
649
+ [source,ruby]
650
+ ----
651
+ renderer = Superfluid.new { it.merge_tags Container, :sample }
652
+ ----
653
+
654
+ Once registered, then you can use as follows:
655
+
656
+ [source,ruby]
657
+ ----
658
+ renderer.call "{% sample %}Your value is: <placeholder>.{% endsample %}"
659
+ # "Your value is: 85."
660
+ ----
661
+
662
+ With containers, you have the full capabilities of the {containable_link}. The above only scratches the surface of what's possible.
663
+
182
664
  == Development
183
665
 
184
666
  To contribute, run:
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "core"
4
+
5
+ module Superfluid
6
+ # The default configuration.
7
+ Configuration = Struct.new(
8
+ :default_resource_limits,
9
+ :error_mode,
10
+ :exception_renderer,
11
+ :file_system,
12
+ :filter_registry,
13
+ :tag_registry
14
+ ) do
15
+ def initialize(**)
16
+ super
17
+
18
+ self[:default_resource_limits] ||= Core::EMPTY_HASH
19
+ self[:error_mode] ||= :strict
20
+ self[:exception_renderer] ||= Core::Identity
21
+ self[:file_system] ||= Systems::Memory.new
22
+ self[:filter_registry] ||= Registries::Filter.new mode: error_mode
23
+ self[:tag_registry] ||= Registries::Tag.new
24
+ end
25
+ end
26
+ end
@@ -1,84 +1,82 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "core"
4
+ require "forwardable"
4
5
 
5
6
  module Superfluid
6
- # The default Liquid environment.
7
- Environment = Struct.new(
8
- :default_resource_limits,
9
- :error_mode,
10
- :exception_renderer,
11
- :file_system,
12
- :filter_registry,
13
- :tags
14
- ) do
15
- def self.for(**)
16
- instance = new(**)
17
-
18
- yield instance if block_given?
19
-
20
- instance.freeze
21
- end
7
+ # The default environment.
8
+ class Environment
9
+ extend Forwardable
22
10
 
23
11
  def self.default
24
12
  @default ||= new
25
13
  end
26
14
 
27
- def initialize(**)
28
- super
15
+ delegate Configuration.members => :configuration
16
+ delegate Configuration.members.map { :"#{it}=" } => :configuration
29
17
 
30
- self[:default_resource_limits] ||= Core::EMPTY_HASH
31
- self[:error_mode] ||= :strict
32
- self[:exception_renderer] ||= Core::Identity
33
- self[:file_system] ||= Systems::Memory.new
34
- self[:filter_registry] ||= Registries::Filter.for mode: error_mode
35
- self[:tags] ||= Liquid::Tags::STANDARD_TAGS.dup
18
+ def self.for(configuration = Configuration, **, &) = new(configuration[**], &)
36
19
 
37
- @filters_cache = {}
38
- end
20
+ def initialize configuration = Configuration.new
21
+ @configuration = configuration
39
22
 
40
- def create_filter_registry _context = nil,
41
- filters = Core::EMPTY_ARRAY,
42
- namespace: Liquid::StandardFilters
43
- return filter_registry if filters.empty?
23
+ yield self if block_given?
24
+ end
44
25
 
45
- filter_registry.add(namespace, *filters)
26
+ def create_filter_registry _context = nil, filters = Core::EMPTY_ARRAY
27
+ filters.empty? ? filter_registry : filter_registry.add(*filters)
46
28
  end
47
29
 
48
- alias_method :create_strainer, :create_filter_registry
30
+ alias create_strainer create_filter_registry
49
31
 
50
- def filter_names = filter_registry.names
32
+ def filter_names = configuration.filter_registry.names
51
33
 
52
- alias_method :filter_method_names, :filter_names
34
+ alias filter_method_names filter_names
53
35
 
54
- def register_filter(namespace, **)
55
- filters_cache.clear
56
- filter_registry.add(namespace, **)
36
+ def merge_filters(container, *)
37
+ filter_registry.merge(container, *)
38
+ self
39
+ end
40
+
41
+ def merge_tags(container, *)
42
+ tag_registry.merge(container, *)
43
+ self
44
+ end
45
+
46
+ def register_filter(*, **)
47
+ filter_registry.add(*, **)
57
48
  self
58
49
  end
59
50
 
60
51
  def register_filters(*, **)
61
- filters_cache.clear
62
52
  filter_registry.add(*, **)
63
53
  self
64
54
  end
65
55
 
66
56
  def register_tag name, klass
67
- tags[name.to_s] = klass
57
+ tag_registry.add name => klass
58
+ self
59
+ end
60
+
61
+ def register_tags(**)
62
+ tag_registry.add(**)
68
63
  self
69
64
  end
70
65
 
71
66
  def find_tag(name) = tags[name.to_s]
72
67
 
73
- alias_method :tag_for_name, :find_tag
68
+ alias tag_for_name find_tag
69
+
70
+ def tags = tag_registry.to_h
74
71
 
75
72
  def freeze
76
- tags.freeze
73
+ filter_registry.freeze
74
+ tag_registry.freeze
77
75
  super
78
76
  end
79
77
 
80
78
  private
81
79
 
82
- attr_reader :filters_cache
80
+ attr_reader :configuration
83
81
  end
84
82
  end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "containable"
4
+ require "core"
5
+ require "json"
6
+ require "refinements/string"
7
+
8
+ module Superfluid
9
+ module Filters
10
+ # A container of filter dependencies.
11
+ module Container
12
+ extend Containable
13
+
14
+ using Refinements::String
15
+
16
+ register(:jsonify) { |value| JSON.generate value }
17
+ register(:parse_json) { |value| JSON.parse value }
18
+
19
+ register :pluralize do |value, count = 0, suffix = "s", replace = /$/|
20
+ "#{count} #{value.pluralize suffix, count, replace:}"
21
+ end
22
+
23
+ register :singularize do |value, count = 0, suffix = "s", replace = Core::EMPTY_STRING|
24
+ "#{count} #{value.singularize suffix, count, replace:}"
25
+ end
26
+
27
+ register :trim_end do |value, maximum, trailer = "..."|
28
+ value.trim_end maximum, nil, trailer:
29
+ end
30
+
31
+ register(:trim_middle) { |value, maximum, gap = "..."| value.trim_middle maximum, gap: }
32
+ end
33
+ end
34
+ end
@@ -1,13 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "core"
4
+ require "functionable"
5
+
3
6
  module Superfluid
4
7
  module Registries
5
- # A filter registry.
6
- Filter = Data.define :mode, :commands do
7
- def self.for(namespace = Liquid::StandardFilters, mode: :strict) = new(mode:).add namespace
8
+ # The default filter registry.
9
+ class Filter
10
+ attr_reader :mode
11
+
12
+ def initialize container = Filters::Container,
13
+ defaults: Liquid::StandardFilters,
14
+ mode: :strict
15
+ @container = container
16
+ @mode = mode
8
17
 
9
- def initialize mode: :strict, commands: {}
10
- super
18
+ add defaults
11
19
  end
12
20
 
13
21
  def add(*, **)
@@ -16,61 +24,74 @@ module Superfluid
16
24
  self
17
25
  end
18
26
 
27
+ def merge(other, *)
28
+ container.merge(other, *)
29
+ self
30
+ end
31
+
19
32
  def call(name, *)
20
33
  dispatch(name, *)
21
34
  rescue ArgumentError => error
22
35
  raise Liquid::ArgumentError, error.message, error.backtrace
23
36
  end
24
37
 
25
- alias_method :invoke, :call
26
-
27
- def clear
28
- commands.clear
29
- self
30
- end
38
+ alias invoke call
31
39
 
32
- def names = commands.keys
40
+ def names = container.keys.sort
33
41
 
34
42
  private
35
43
 
36
- def add_namespaces(*all) = all.each { add_methods it }
44
+ attr_reader :container
45
+
46
+ def add_namespaces(*collection) = collection.each { add_methods it }
37
47
 
38
48
  def add_methods namespace
49
+ # Order matters.
50
+ case namespace
51
+ in Class
52
+ fail Liquid::ArgumentError,
53
+ "Invalid type (#{namespace}) for namespace. Must be a Module or Functionable."
54
+ in Functionable then add_function_methods namespace
55
+ in Module then add_instance_methods namespace
56
+ # simplecov:disable
57
+ end
58
+ end
59
+
60
+ def add_function_methods namespace
61
+ namespace.function_methods
62
+ .each
63
+ .with_object({}) { |name, all| all[name] = namespace.method name }
64
+ .then { add_commands(**it) }
65
+ end
66
+
67
+ def add_instance_methods namespace
39
68
  shadow = Class.new.include(namespace).new
40
69
 
41
- all = namespace.instance_methods
42
- .each
43
- .with_object({}) do |method, all|
44
- name = method.name
45
- all[name] = shadow.method name
46
- end
70
+ collection = namespace.instance_methods
71
+ .each
72
+ .with_object({}) do |method, all|
73
+ name = method.name
74
+ all[name] = shadow.method name
75
+ end
47
76
 
48
- add_commands(**all)
77
+ add_commands(**collection)
49
78
  end
50
79
 
51
- def add_commands(**all) = all.each { |name, command| add_command name, command }
80
+ def add_commands(**collection) = collection.each { |name, command| add_command name, command }
52
81
 
53
- # :reek:ManualDispatch
54
- def add_command name, function
55
- key = name.to_s
82
+ def add_command name, callable
83
+ return if container.key? name
56
84
 
57
- return if commands.key? key
58
-
59
- case function
60
- in Proc | Method | Object if function.respond_to? :call then commands[key] = function
85
+ case callable
86
+ in Proc | Method | Core::Composable then container.register(name) { callable }
61
87
  else fail Liquid::ArgumentError, "Filter must be callable."
62
88
  end
63
89
  end
64
90
 
65
- def dispatch name, *positionals
66
- key = name.to_s
67
-
68
- if commands.key? key
69
- commands[key].call(*positionals)
70
- elsif mode == :strict
71
- fail Liquid::UndefinedFilter, "Undefined filter: #{name}."
72
- else
73
- positionals.first
91
+ def dispatch name, *arguments
92
+ if container.key? name then container[name].call(*arguments)
93
+ elsif mode == :strict then fail Liquid::UndefinedFilter, "Undefined filter: #{name}."
94
+ else arguments.first
74
95
  end
75
96
  end
76
97
  end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Superfluid
4
+ module Registries
5
+ # The default tag registry.
6
+ class Tag
7
+ def initialize container = Tags::Container, defaults: Liquid::Tags::STANDARD_TAGS
8
+ @container = container
9
+
10
+ add(**defaults)
11
+ end
12
+
13
+ def add **collection
14
+ collection.each { |name, tag| container.register name, tag unless container.key? name }
15
+ self
16
+ end
17
+
18
+ def merge(other, *)
19
+ container.merge(other, *)
20
+ self
21
+ end
22
+
23
+ def get(name) = (container[name] if container.key? name)
24
+
25
+ def names = container.keys.sort
26
+
27
+ def to_h = container.each.to_h
28
+
29
+ private
30
+
31
+ attr_reader :container
32
+ end
33
+ end
34
+ end
@@ -1,14 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "core"
4
+
3
5
  module Superfluid
4
6
  # Renders template and associated data.
5
7
  class Renderer
6
- def initialize environment: Environment.for, parser: Liquid::Template
8
+ def initialize environment: Environment.new, parser: Liquid::Template
7
9
  @environment = environment
8
10
  @parser = parser
11
+
12
+ yield environment if block_given?
13
+
14
+ environment.freeze
9
15
  end
10
16
 
11
- def call(template, data) = parser.parse(template, environment:).render data
17
+ def call(template, data = Core::EMPTY_HASH) = parser.parse(template, environment:).render data
12
18
 
13
19
  private
14
20
 
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "containable"
4
+
5
+ module Superfluid
6
+ module Tags
7
+ # A container of tag dependencies.
8
+ module Container
9
+ extend Containable
10
+
11
+ register :template, Template
12
+ end
13
+ end
14
+ end
data/lib/superfluid.rb CHANGED
@@ -19,5 +19,5 @@ module Superfluid
19
19
 
20
20
  def self.build(**) = Environment.for(**) { yield it if block_given? }
21
21
 
22
- def self.new(**) = Renderer.new(**)
22
+ def self.new(**, &) = Renderer.new(**, &)
23
23
  end
data/superfluid.gemspec CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Gem::Specification.new do |spec|
4
4
  spec.name = "superfluid"
5
- spec.version = "0.0.0"
5
+ spec.version = "0.2.0"
6
6
  spec.authors = ["Brooke Kuhlmann"]
7
7
  spec.email = ["brooke@alchemists.io"]
8
8
  spec.homepage = "https://alchemists.io/projects/superfluid"
@@ -25,7 +25,9 @@ Gem::Specification.new do |spec|
25
25
  spec.required_ruby_version = ">= 4.0"
26
26
 
27
27
  spec.add_dependency "base64", "~> 0.3"
28
- spec.add_dependency "core", "~> 3.3"
28
+ spec.add_dependency "containable", "~> 2.5"
29
+ spec.add_dependency "core", "~> 3.4"
30
+ spec.add_dependency "functionable", "~> 1.4"
29
31
  spec.add_dependency "liquid", "~> 5.13"
30
32
  spec.add_dependency "refinements", "~> 14.0"
31
33
  spec.add_dependency "zeitwerk", "~> 2.8"
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: superfluid
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brooke Kuhlmann
@@ -49,20 +49,48 @@ dependencies:
49
49
  - - "~>"
50
50
  - !ruby/object:Gem::Version
51
51
  version: '0.3'
52
+ - !ruby/object:Gem::Dependency
53
+ name: containable
54
+ requirement: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - "~>"
57
+ - !ruby/object:Gem::Version
58
+ version: '2.5'
59
+ type: :runtime
60
+ prerelease: false
61
+ version_requirements: !ruby/object:Gem::Requirement
62
+ requirements:
63
+ - - "~>"
64
+ - !ruby/object:Gem::Version
65
+ version: '2.5'
52
66
  - !ruby/object:Gem::Dependency
53
67
  name: core
54
68
  requirement: !ruby/object:Gem::Requirement
55
69
  requirements:
56
70
  - - "~>"
57
71
  - !ruby/object:Gem::Version
58
- version: '3.3'
72
+ version: '3.4'
73
+ type: :runtime
74
+ prerelease: false
75
+ version_requirements: !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - - "~>"
78
+ - !ruby/object:Gem::Version
79
+ version: '3.4'
80
+ - !ruby/object:Gem::Dependency
81
+ name: functionable
82
+ requirement: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - "~>"
85
+ - !ruby/object:Gem::Version
86
+ version: '1.4'
59
87
  type: :runtime
60
88
  prerelease: false
61
89
  version_requirements: !ruby/object:Gem::Requirement
62
90
  requirements:
63
91
  - - "~>"
64
92
  - !ruby/object:Gem::Version
65
- version: '3.3'
93
+ version: '1.4'
66
94
  - !ruby/object:Gem::Dependency
67
95
  name: liquid
68
96
  requirement: !ruby/object:Gem::Requirement
@@ -116,10 +144,14 @@ files:
116
144
  - LICENSE.adoc
117
145
  - README.adoc
118
146
  - lib/superfluid.rb
147
+ - lib/superfluid/configuration.rb
119
148
  - lib/superfluid/environment.rb
149
+ - lib/superfluid/filters/container.rb
120
150
  - lib/superfluid/registries/filter.rb
151
+ - lib/superfluid/registries/tag.rb
121
152
  - lib/superfluid/renderer.rb
122
153
  - lib/superfluid/systems/memory.rb
154
+ - lib/superfluid/tags/container.rb
123
155
  - lib/superfluid/tags/template.rb
124
156
  - superfluid.gemspec
125
157
  homepage: https://alchemists.io/projects/superfluid
@@ -147,7 +179,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
147
179
  - !ruby/object:Gem::Version
148
180
  version: '0'
149
181
  requirements: []
150
- rubygems_version: 4.0.19
182
+ rubygems_version: 4.0.20
151
183
  specification_version: 4
152
184
  summary: Enhances Liquid with object composition and functional design.
153
185
  test_files: []
metadata.gz.sig CHANGED
Binary file