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):

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:

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:

$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:

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.

use MVC::Keayl::Helpers::Form;

FormBuilder.new(object-name => 'post').rich-text-area('body');
<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):

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:

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.

<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.