Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 110 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ Lexxy.configure({ global: { extensions: [ VariableExtension ] } })

`catalog` is the list users pick from in the editor. `assigns` is the lookup that
turns a key into a value at render time. `catalog` is required and `assigns` is
optional. Leave it out and the gem reads `value` straight off the catalog item.
optional. Leave it out and the gem reads `value` straight off the catalog item,
or supply values per render (see [Examples](#examples)).

Put the `configure` block in an initializer, e.g. `config/initializers/lexxy_variables.rb`:

Expand Down Expand Up @@ -121,11 +122,99 @@ Each chip resolves to its value, so the reader sees finished text:
`@record` and `:body` are placeholders. Use whatever model and Action Text
attribute hold your content.

## Examples

### Simple replacement from a model

First and last name variables, with the values supplied at render time. No Liquid needed. The `catalog` lists the two variables so they appear in the editor:

```ruby
# config/initializers/lexxy_variables.rb
LexxyVariables.configure do |c|
c.catalog = [
{ key: "first_name", name: "First name" },
{ key: "last_name", name: "Last name" }
]
end
```

Editor page:

```erb
<%= form_with model: @message do |form| %>
<%= form.rich_text_area :body do %>
<%= lexxy_variables_prompt %>
<% end %>
<%= form.submit %>
<% end %>
```

Display page:

```erb
<%= render_lexxy_content(@message.body,
first_name: @user.first_name,
last_name: @user.last_name) %>
```

The catalog entries have no `value` because it's supplied at render time, so the same saved content renders differently per user. The default renderer escapes the values automatically. When a variable name would collide with `context:` or `locale:`, use an `assigns:` hash instead (see [Helper options](#helper-options)).

### Static values from config

When a value is the same for everyone, it can live on the catalog item itself. With no `assigns` set, the gem reads `value` off the catalog and the helper takes no extra arguments:

```ruby
LexxyVariables.configure do |c|
c.catalog = [ { key: "company", name: "Company", value: "Acme" } ]
end
```

```erb
<%= render_lexxy_content(@message.body) %>
```

### Liquid drops and dotted access

Liquid enables dotted access like `{{ user.first_name }}`, filters, and drops that expose a whole object. A drop is a small class that defines which methods Liquid can call, and escapes what it returns:

```ruby
# app/drops/user_drop.rb
# Liquid output is emitted html_safe after sanitization,
# so a drop must escape its own values.
class UserDrop < Liquid::Drop
def initialize(user) = @user = user

def first_name = ERB::Util.html_escape(@user.first_name).to_s
def full_name = ERB::Util.html_escape(@user.full_name).to_s
def email = ERB::Util.html_escape(@user.email).to_s
end
```

The dotted keys go in the catalog so they can be inserted, the Liquid renderer is switched on, and `assigns` returns the drop under the name the keys use (`user`):

```ruby
LexxyVariables.configure do |c|
c.catalog = [
{ key: "user.first_name", name: "First name" },
{ key: "user.full_name", name: "Full name" },
{ key: "user.email", name: "Email" }
]

c.renderer = LexxyVariables::Renderers::Liquid.new

c.assigns = ->(_used_keys) { { "user" => UserDrop.new(Current.user) } }
end
```

```erb
<%= render_lexxy_content(@message.body) %>
```

A `user.first_name` chip becomes `{{ user.first_name }}`, which Liquid runs through the drop. Only the methods defined on the drop are reachable, not arbitrary attributes on the user. Liquid doesn't escape output the way the default renderer does, so the drop escapes its own values. The same goes for any plain string returned from `assigns` or passed inline.

## Full configuration

`context` is yours to define. The gem passes it untouched to your catalog,
assigns, and resolve callables, so put whatever they need in it. That might be a
tenant, `nil`, or any object.
`context` is yours to define. The gem passes it untouched to your catalog, assigns, and resolve callables, so put whatever they need in it. That might be a tenant, `nil`, or any object.

```ruby
LexxyVariables.configure do |c|
Expand Down Expand Up @@ -153,7 +242,7 @@ end
| Option | Default | What it does |
| --- | --- | --- |
| `catalog` | `[]` | The insertable items shown in the `{{` prompt and the toolbar dropdown. A list, a zero-arg lambda, or a `->(context)` lambda. Items respond to `#key` and `#name`, and optionally `#value` and `#attachable_sgid`. |
| `assigns` | reads `#value` off catalog items | The render-time lookup. A `->(context, used_keys)` or `->(used_keys)` lambda that receives only the keys used in the content being rendered and returns a `{ key => value }` hash. |
| `assigns` | reads `#value` off catalog items | The render-time lookup. A `->(context, used_keys)` or `->(used_keys)` lambda that receives only the keys used in the content being rendered and returns a `{ key => value }` hash. Per-render values can also be passed straight to `render_lexxy_content` (see [Helper options](#helper-options)). |
| `renderer` | `Renderers::Substitution.new` | How placeholders become values. The default is plain, escaped string substitution with no template engine. Swap in `Renderers::Liquid.new` for dotted access, drops, and filters. |
| `sort` | `:name` | How the catalog is ordered in the prompt and dropdown. `:name` (case-insensitive alphabetical), `:key`, `false` to keep the catalog's given order, or a lambda (a `->(item)` sort key or a `->(a, b)` comparator). |
| `max_fragment_depth` | `1` | How many levels of `renders_as: :html` chips expand. The default resolves the variables inside a snippet but drops a snippet nested inside another snippet. Raise it to allow deeper nesting. |
Expand All @@ -173,6 +262,22 @@ wrapping the whole pass in `I18n.with_locale`.
<%= render_lexxy_content(@record.body, locale: recipient.locale) %>
```

You can also pass a variable's value straight to `render_lexxy_content`, as
keyword arguments or an `assigns:` hash. These win over whatever the configured
`assigns` returns, and a value for a key that isn't used in the content is just
ignored:

```erb
<%# keyword arguments %>
<%= render_lexxy_content(@record.body, first_name: @user.first_name) %>

<%# same thing, for a name that would clash with context: or locale: %>
<%= render_lexxy_content(@record.body, assigns: { first_name: @user.first_name }) %>
```

The default renderer escapes these values for you. Liquid doesn't, so escape
them yourself or pass a drop.

## Multi-tenancy

Tenancy is optional. If your app is multi-tenant, pass the tenant through as
Expand Down
Loading