Skip to content

Commit

Permalink
Helper DSL for plugins (#135)
Browse files Browse the repository at this point in the history
* Add a helper DSL method for builder plugins

* Add helpers plugin documentation

* Reflow the ERB and Beyond documentation

* Excute helper blocks in builder scope by default

* Use correct helper terminology

* Switch to using instance_exec
  • Loading branch information
jaredcwhite authored Sep 17, 2020
1 parent c8a72d8 commit 2ac8e2b
Show file tree
Hide file tree
Showing 7 changed files with 254 additions and 53 deletions.
27 changes: 27 additions & 0 deletions bridgetown-builder/lib/bridgetown-builder/dsl/helpers.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# frozen_string_literal: true

module Bridgetown
module Builders
module DSL
module Helpers
def helper(helper_name, method_name = nil, helpers_scope: false, &block)
builder_self = self
m = Module.new

if block && !helpers_scope
m.define_method helper_name do |*args|
builder_self.instance_exec(*args, &block)
end
else
block = method(method_name) if method_name
m.define_method helper_name, &block
end

Bridgetown::RubyTemplateView::Helpers.include(m)

functions << { name: name, filter: m }
end
end
end
end
end
2 changes: 2 additions & 0 deletions bridgetown-builder/lib/bridgetown-builder/plugin.rb
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
# frozen_string_literal: true

require "bridgetown-builder/dsl/generators"
require "bridgetown-builder/dsl/helpers"
require "bridgetown-builder/dsl/hooks"
require "bridgetown-builder/dsl/http"
require "bridgetown-builder/dsl/liquid"
module Bridgetown
module Builders
class PluginBuilder
include DSL::Generators
include DSL::Helpers
include DSL::Hooks
include DSL::HTTP
include DSL::Liquid
Expand Down
69 changes: 69 additions & 0 deletions bridgetown-builder/test/test_helpers_dsl.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# frozen_string_literal: true

require "helper"

class HelpersBuilder < Builder
def build
helper "block_based" do |something|
"Block Based #{something} #{self.class}"
end

helper "within_helpers_scope", helpers_scope: true do |something|
"Within Helpers Scope Based #{something} #{self.class} #{slugify("I Am Groot")}"
end

helper "method_based", :method_based
end

def method_based(something)
"Method Based #{something} #{self.class}"
end
end

class TestHelpers < BridgetownUnitTest
context "adding helpers" do
setup do
Bridgetown.sites.clear
@site = Site.new(site_configuration)
Builders::DocumentsGenerator.clear_documents_to_generate
@generator = Builders::DocumentsGenerator.new(@site.config)
@builder = HelpersBuilder.new("HelpersBuilder", @site)
Builders::DocumentsGenerator.add("im-a-post.md", proc {
title "I'm a post!"
date "2019-05-01"
})
@generator.generate(@site)
@erb_view = Bridgetown::ERBView.new(@site.posts.docs.first)
end

should "work with blocks" do
content = "This is the <%= block_based page[:title] %> helper"
tmpl = Tilt::ErubiTemplate.new(
outvar: "@_erbout",
engine_class: Erubi::CaptureEndEngine
) { content }
result = tmpl.render(@erb_view)
assert_equal "This is the Block Based I'm a post! HelpersBuilder helper", result
end

should "allow execution within helpers scope" do
content = "This is the <%= within_helpers_scope page[:title] %> helper"
tmpl = Tilt::ErubiTemplate.new(
outvar: "@_erbout",
engine_class: Erubi::CaptureEndEngine
) { content }
result = tmpl.render(@erb_view)
assert_equal "This is the Within Helpers Scope Based I'm a post! Bridgetown::RubyTemplateView::Helpers i-am-groot helper", result
end

should "work with methods" do
content = "This is the <%= method_based page[:title] %> helper"
tmpl = Tilt::ErubiTemplate.new(
outvar: "@_erbout",
engine_class: Erubi::CaptureEndEngine
) { content }
result = tmpl.render(@erb_view)
assert_equal "This is the Method Based I'm a post! HelpersBuilder helper", result
end
end
end
96 changes: 51 additions & 45 deletions bridgetown-website/src/_docs/erb-and-beyond.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Bridgetown's primary template language is [**Liquid**](/docs/liquid), due to his

However, Bridgetown's implementation language, Ruby, has a rich history of promoting [ERB (Embedded RuBy)](https://docs.ruby-lang.org/en/2.7.0/ERB.html) for templates and view layers across a wide variety of tools and frameworks, and other template languages such as [Haml](http://haml.info) and [Slim](http://slim-lang.com) boast their fair share of enthusiasts.

So, starting with Bridgetown 0.16, you can now add ERB-based templates and pages (and partials too) to your site. In additional, there are plugins you can easily install for Haml and Slim as well.
So, starting with Bridgetown 0.16, you can now add ERB-based templates and pages (and partials too) to your site. In additional, there are plugins you can easily install for Haml and Slim as well. Under the hood, Bridgetown uses the [Tilt gem](https://github.com/rtomayko/tilt) to load and process these Ruby templates.

{% toc %}

Expand Down Expand Up @@ -69,50 +69,7 @@ Bridgetown includes access to some helpful [custom Liquid filters](/docs/liquid/
<%= date_to_string site.time, "ordinal" %>
```

These helpers are actually methods of the `helper` object which is an instance of `Bridgetown::RubyTemplateView::Helpers`. If you wanted to add your own custom helpers to ERB templates, you could open the class up in a plugin and define additional methods:

```ruby
# plugins/site_builder.rb

Bridgetown::RubyTemplateView::Helpers.class_eval do
def uppercase_string(input)
input.upcase
end
end
```

```eruby
<%= uppercase_string "i'm a string" %>
<!-- output: I'M A STRING -->
```

As a best practice, it would be best to define your helpers as methods of a dedicated `Module` which could then be used for both Liquid filters and ERB helpers simultaneously. Here's how you might go about that in your plugin:

```ruby
# plugins/filters.rb

module MyFilters
def lowercase_string(input)
input.downcase
end
end

Liquid::Template.register_filter MyFilters
Bridgetown::RubyTemplateView::Helpers.include MyFilters
```

Usage is pretty straightforward:

{% raw %}
```eruby
<%= lowercase_string "WAY DOWN LOW" %>
```

```Liquid
{{ "WAY DOWN LOW" | lowercase_string }}
```
{% endraw %}
These helpers are actually methods of the `helper` object which is an instance of `Bridgetown::RubyTemplateView::Helpers`.

In addition to using Liquid helpers, you can also render [Liquid components](/docs/components) from within your ERB templates via the `liquid_render` helper.

Expand Down Expand Up @@ -242,6 +199,55 @@ permalink: /posts.json

The ensures the final relative URL will be `/posts.json`. (Of course you can also set the permalink to anything you want, regardless of the filename itself.)

## Custom Helpers

If you'd like to add your own custom template helpers, you can use the `helper` DSL within builder plugins. [Read this documentation to learn more](/docs/plugins/helpers).

Alternatively, you could open up the `Helpers` class and define additional methods:

```ruby
# plugins/site_builder.rb

Bridgetown::RubyTemplateView::Helpers.class_eval do
def uppercase_string(input)
input.upcase
end
end
```

```eruby
<%= uppercase_string "i'm a string" %>
<!-- output: I'M A STRING -->
```

As a best practice, it would be best to define your helpers as methods of a dedicated `Module` which could then be used for both Liquid filters and ERB helpers simultaneously. Here's how you might go about that in your plugin:

```ruby
# plugins/filters.rb

module MyFilters
def lowercase_string(input)
input.downcase
end
end

Liquid::Template.register_filter MyFilters
Bridgetown::RubyTemplateView::Helpers.include MyFilters
```

Usage is pretty straightforward:

{% raw %}
```eruby
<%= lowercase_string "WAY DOWN LOW" %>
```

```Liquid
{{ "WAY DOWN LOW" | lowercase_string }}
```
{% endraw %}

## Haml and Slim

Bridgetown comes with ERB support out-of-the-box, but you can easily add support for either Haml or Slim by installing our officially supported plugins.
Expand Down
18 changes: 11 additions & 7 deletions bridgetown-website/src/_docs/plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,31 +157,35 @@ Documentation on the internal Ruby API for Bridgetown is forthcoming, but meanwh

There are several categories of functionality you can add to your Bridgetown plugin:

### [Tags](/docs/plugins/tags/)
### [Tags](/docs/plugins/tags)

Create custom Liquid tags or "shortcodes" which you can add to your content or design templates.

### [Filters](/docs/plugins/filters/)
### [Filters](/docs/plugins/filters)

Create custom Liquid filters to help transform data and content.

### [HTTP Requests and the Document Builder](/docs/plugins/external-apis/)
### [Helpers](/docs/plugins/helpers)

For Tilt-based templates such as [ERB, Slim, etc.](/docs/erb-and-beyond), you can provide custom helpers which can be called from your templates.

### [HTTP Requests and the Document Builder](/docs/plugins/external-apis)

Easily pull data in from external APIs, and use a special DSL (Domain-Specific Language) to build documents out of that data.

### [Hooks](/docs/plugins/hooks/)
### [Hooks](/docs/plugins/hooks)

Hooks provide fine-grained control to trigger custom functionality at various points in the build process.

### [Generators](/docs/plugins/generators/)
### [Generators](/docs/plugins/generators)

Generators allow you to automate the creating or updating of content in your site using Bridgetown's internal Ruby API.

### [Commands](/docs/plugins/commands/)
### [Commands](/docs/plugins/commands)

Commands extend the `bridgetown` executable using the Thor CLI toolkit.

### [Converters](/docs/plugins/converters/)
### [Converters](/docs/plugins/converters)

Converters change a markup language from one format to another.

Expand Down
2 changes: 1 addition & 1 deletion bridgetown-website/src/_docs/plugins/filters.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ And of course you can chain any number of built-in and custom filters together:
As with other parts of the Builder API, you can also use an instance method to register your filter:

```ruby
def Upcase < SiteBuilder
def Filters < SiteBuilder
def build
liquid_filter "cache_busting_url", :bust_it
end
Expand Down
93 changes: 93 additions & 0 deletions bridgetown-website/src/_docs/plugins/helpers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
---
title: Helpers
hide_in_toc: true
order: 0
category: plugins
---

Helpers are Ruby methods you can provide to Tilt-based templates ([ERB, Slim, etc.](/docs/erb-and-beyond)) to transform data or output new content in various ways.

Example:

```ruby
class Helpers < SiteBuilder
def build
helper "cache_busting_url" do |url|
"http://www.example.com/#{url}?#{Time.now.to_i}"
end
end
end
```

```erb
<%= cache_busting_url "mydynamicfile.js" %>
```

outputs:

```
http://www.example.com/mydynamicfile.js?1586194585
```

## Supporting Arguments

You can accept multiple arguments to your helper by simply adding them to your block or method, and optional ones are simply specified with a default value (perhaps `nil` or `false`). For example:

```ruby
def Helpers < SiteBuilder
def build
helper "multiply_and_optionally_add" do |input, multiply_by, add_by = nil|
value = input * multiply_by
add_by ? value + add_by : value
end
end
end
```

Then just use it like this:

```erb
5 times 10 equals <%= multiply_and_optionally_add 5, 10 %>
output: 5 times 10 equals 50
5 times 10 plus 3 equals <%= multiply_and_optionally_add 5, 10, 3 %>
output: 5 times 10 plus 3 equals 53
```

## Using Instance Methods

As with other parts of the Builder API, you can also use an instance method to register your helper:

```ruby
def Helpers < SiteBuilder
def build
helper "cache_busting_url", :bust_it
end

def bust_it(url)
"http://www.example.com/#{url}?#{Time.now.to_i}"
end
end
```

## Helper Execution Scope

By default, the code within the helper block or method is executed within the scope of the builder object. This means you will not have access to other helpers you may expecting to call. For example, if you want to call `slugify` from your helper, it will cause an error.

To remedy this, simply pass the `helpers_scope: true` argument when defining a helper block. Then you can call other helpers as part of your code block (but not methods within your builder).

```ruby
def Helpers < SiteBuilder
def build
helper "slugify_and_upcase", helpers_scope: true do |url|
slugify(url).upcase
end
end
end
```

## Helpers vs. Filters vs. Tags

Filters and tags are aspects of the [Liquid](/docs/liquid) template engine which comes installed by default. The behavior of both filters and tags are roughly analogous to helpers in [Tilt-based templates](/docs/erb-and-beyond). Specialized Bridgetown filters are also made available as helpers, as are a few tags such as `webpack_path`.

0 comments on commit 2ac8e2b

Please sign in to comment.