Skip to content

Action Text

Action Text adds rich-text content to a model. Content is stored as sanitized HTML against an allowlist, edited through a Trix editor, rendered with safe-buffer output, and can embed Active Storage attachments.

Rich-text content

A MVC::Keayl::ActionText::Content wraps a fragment of rich-text HTML. Content.from-html sanitizes the input, keeping the rich-text allowlist of tags and attributes and dropping everything else (scripts, event handlers, unknown tags):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
use MVC::Keayl::ActionText;

my $content = MVC::Keayl::ActionText::Content.from-html(
  '<h1>Title</h1><script>evil()</script>'
);

$content.to-trix-html;   # '<h1>Title</h1>' as a safe string, for editing
$content.to-html;        # rendered html (attachments expanded), a safe string
$content.to-plain-text;  # 'Title'
$content.is-empty;       # False

sanitize-rich-text($html) runs the same allowlist on its own.

Attaching rich text to a record

A model includes MVC::Keayl::ActionText::RichTextable and declares its rich-text fields with has-rich-text:

1
2
3
4
5
6
use MVC::Keayl::ActionText::RichTextable;

class Article does RichTextable {
  has $.id;
}
Article.has-rich-text('content');

Each declared name returns a proxy backed by a rich-text record:

1
2
3
4
$article.content.assign('<h1>Hello</h1>');   # sanitizes and stores
$article.content.is-present;                  # True
$article.content.to-html;                     # the rendered html
$article.content.to-plain-text;               # 'Hello'

Records persist through a repository. MVC::Keayl::ActionText::Repository defines the interface and MemoryRepository is the in-process implementation. Configure an ORM-backed repository at boot:

1
set-rich-text-repository(MemoryRepository.new);

Editing

rich-text-area on the form builder emits the Trix editor markup: a hidden input that carries the value plus a <trix-editor> bound to it.

1
2
3
use MVC::Keayl::Helpers::Form;

FormBuilder.new(object-name => 'post').rich-text-area('body');
1
2
<input type="hidden" id="post_body_trix_input" name="post[body]" value="...">
<trix-editor input="post_body_trix_input" class="trix-content"></trix-editor>

rich-text-area-tag(name, value?, %options) renders the same markup without a form builder.

Rendering

The rich-text helper renders content for a view with safe-buffer output. It accepts a Content, a rich-text proxy, or a raw HTML string (which it sanitizes):

1
2
rich-text($article.content);
rich-text('<p>hello</p>');

Embedded attachments

An Active Storage blob can be embedded in the document as an <action-text-attachment> element carrying a signed blob id. embed-tag($blob) builds one:

1
2
my $tag = embed-tag($blob);
# <action-text-attachment sgid="..." content-type="image/png" filename="photo.png"></action-text-attachment>

When the content renders, each attachment resolves its signed id to a blob and expands into a <figure>. An image becomes a preview pointing at the proxy serving path; any other file becomes a download link. An attachment whose signed id no longer resolves is dropped.

1
2
3
<figure class="attachment attachment--preview attachment--png">
  <img src="/keayl/blobs/proxy/..." alt="photo.png">
</figure>

content.attachment-sgids lists the signed ids embedded in a document.