Implementing custom liquid tags
Tags: rails, liquidtags, forem
Published: 2020-11-12 04:32:28

Liquid tags are awesome dev.to uses some custom liquid tags to embed posts, gist, github repos, twitch streams and lot more. To understand how liquid tag works in dev.to I tried to implement similar custom liquid tag. 

We will build a gist liquid tag. That renders a gist when provided with url like below

{% gist https://gist.github.com/Rafi993/4f559b78316ca74b0101ace7bda0f889 %}

You can get the boiler plate for this project from [here](https://github.com/Rafi993/custom-liquid-tags/tree/boilerplate) in **boilerplate** branch

```
$ git clone https://github.com/Rafi993/custom-liquid-tags
$ cd custom-liquid-tags
$ git checkout boilerplate
$ bin/setup
```
It provides you with basic routing, layout and CSS.

## Liquid template language
Liquid is templating language created by shopify (similar to erb). We will include liquid [gem](https://rubygems.org/gems/liquid) in the project by adding liquid in the **Gemfile** and running **bundle install**

```
gem 'liquid', '~> 4.0', '>= 4.0.3'
```
to parse liquid tags in user content we can do the following in the **preview** action

```ruby
  def preview
    @template = Liquid::Template.parse(params[:text])
    @parsed_html = @template.render()
    render "preview"
  end
```
and liquid tags should work now

![abs tag](https://dev-to-uploads.s3.amazonaws.com/i/rudipbt50j6g0q3b64dw.png)

![abs tag output](https://dev-to-uploads.s3.amazonaws.com/i/0aktq6qnxp3uj0q4fbl6.png)

we will disable these default tags later

## Custom tags
First we will create a **app/liquid_tags/liquid_tag_base.rb**
from which all the liquid tags will inherit from. 
```ruby
class LiquidTagBase < Liquid::Tag
  def self.script
    ""
  end

  def initialize(_tag_name, _content, parse_context)
    super
  end
end
```
If you have some common code that needs to be shared across all liquid tags you can place it here.

Then we will create our gist liquid tag **app/liquid_tags/gist_tag.rb** we could copy the code from Forem repo for this :)

```ruby
class GistTag < LiquidTagBase
  PARTIAL = "liquids/gist".freeze
  VALID_LINK_REGEXP =
    %r{\Ahttps://gist\.github\.com/([a-zA-Z0-9](-?[a-zA-Z0-9]){0,38})/([a-zA-Z0-9]){1,32}(/[a-zA-Z0-9]+)?\Z}
      .freeze

  def initialize(_tag_name, link, _parse_context)
    super

    raise StandardError, "Invalid Gist link: You must provide a Gist link" if link.blank?

    @uri = build_uri(link)
  end

  def render(_context)
    ApplicationController.render(
      partial: PARTIAL,
      locals: {
        uri: @uri,
      },
    )
  end

  private

  def build_uri(link)
    link = ActionController::Base.helpers.strip_tags(link)
    link, option = link.split(" ", 2)
    link = parse_link(link)

    uri = "#{link}.js"
    uri += build_options(option) unless option&.empty?

    uri
  end

  def parse_link(link)
    input_no_space = link.delete(" ").gsub(".js", "")
    if valid_link?(input_no_space)
      input_no_space
    else
      raise StandardError,
            "Invalid Gist link: #{link} Links must follow this format: https://gist.github.com/username/gist_id"
    end
  end

  def build_options(option)
    option_no_space = option.strip
    return "?#{option_no_space}" if valid_option?(option_no_space)

    raise StandardError, "Invalid Filename"
  end

  def valid_link?(link)
    (link =~ VALID_LINK_REGEXP)&.zero?
  end

  def valid_option?(option)
    (option =~ /\Afile=[^\\]*(\.(\w+))?\Z/)&.zero?
  end
end

Liquid::Template.register_tag("gist", GistTag)

```

The above code parses provided argument to gist liquid tag and generates proper uri that it can use. It also specifies which partial to use for the liquid tag.

we will create that partial file that says how to render this tag **app/views/liquids/_gist.html.erb**

```erb
<div>
  <script id="gist-ltag" src="<%= uri %>"></script>
</div>
```

If we actually try to use the gist liquid tag now it will throw error saying **tag gist is not defined**

## Initializing liquid tag
We can create our initializer in **config/initializers/liquid.rb** that loads our custom liquid tags

```ruby
Rails.application.config.to_prepare do

  # Custom Liquid tags are loaded here
  Dir.glob(Rails.root.join("app/liquid_tags/*.rb")).sort.each do |filename|
    require_dependency filename
  end
end
```
The above code looks for liquid_tags in the specified path and loads them.

Now when we try our gist liquid tag it should work

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/qo5rftwok24iblx1fjir.png)

## Disabling default tags
You may not want default tags like **assign**, **raw** to work. You could disable those by adding the following code to **config/initializers/liquid.rb**

```ruby
  # Tags to disable
  Dir.glob(Rails.root.join("lib/liquid/*.rb")).sort.each do |filename|
    require_dependency filename
  end
```

and defining those tags there inside **lib/liquid** folder

```ruby
# lib/liquid/raw.rb
module Liquid
  class Raw < Block
    remove_const(:FullTokenPossiblyInvalid) if defined?(FullTokenPossiblyInvalid)
    FullTokenPossiblyInvalid = /\A(.*)#{TagStart}\s*(\w+)\s*#{TagEnd}\z/om.freeze # rubocop:disable Naming/ConstantName
  end

  Template.register_tag("raw", Raw)
end
```

```ruby
# lib/liquid/variables.rb
module Liquid
  class Variable
    def initialize(_markup, _parse_context)
      raise StandardError, "Liquid variables are disabled"
    end
  end
end

```

Now you should not able to use default tags like **assign**, **abs** ...

You can find the source for the project [here](https://github.com/Rafi993/custom-liquid-tags/tree/main)

**Note:**
1. Forem does little more when by checking if the user can access a liquid tag using pundit gem

cover image by Vitaly Vlasov from Pexels