tonysm/rich-text-laravel

将 Trix 内容与 Laravel 集成

3.1.0 2024-04-14 21:19 UTC

README

Logo Rich Text Laravel

Total Downloads License

Trix 编辑器 与 Laravel 集成。灵感来自 Rails 的 Action Text。

安装

您可以通过 composer 安装此包

composer require tonysm/rich-text-laravel

然后,运行以下命令进行安装

php artisan richtext:install

接下来,运行迁移

php artisan migrate

确保将样式 Blade 组件添加到您的布局中

<x-rich-text::styles />

如果您使用 Breeze(或 TailwindCSS),您可能更喜欢经过调整的主题

<x-rich-text::styles theme="richtextlaravel" />

最后,您现在可以在表单上像这样使用已发布的输入 Blade 组件

<x-trix-input id="bio" name="bio" />

就是这样!

概述

我们在保存数据库中的丰富文本字段(使用 Trix)之前提取附件,并最小化内容以进行存储。附件将被替换为 rich-text-attachment 标签。从可附加模型中的附件具有 sgid 属性,这应在您的应用程序中全局识别它们。

当直接存储图像时(例如,在简单的图像上传中,您没有在应用程序中表示该附件的模型),我们将在 rich-text-attachment 中填充所有必要的附件属性以再次渲染该图像。存储最小化(规范)的丰富文本内容意味着我们不会存储附件标签的内部内容,只存储在需要时再次渲染它所需的元数据。

有两种使用此包的方式

  1. 使用推荐的数据库结构,其中所有丰富文本内容都将存储在具有丰富文本内容的模型之外(推荐);以及
  2. 仅使用 AsRichTextContent 特性将任何模型的任何表上的丰富文本内容字段进行类型转换。

以下我们将介绍每种使用方法。建议您至少阅读一些时候的 Trix 文档 以获得其客户端的概述。

RichText 模型

推荐的方式是将丰富文本内容本身之外保持。这将使您在操作模型时保持模型精简,并且您可以在需要丰富文本内容的地方(贪婪地或懒加载地)仅加载丰富文本字段。

以下是如何在 Post 模型上创建两个丰富文本字段,例如,您需要一个用于内容正文,另一个用于内部备注

use Tonysm\RichTextLaravel\Models\Traits\HasRichText;

class Post extends Model
{
    use HasRichText;

    protected $guarded = [];

    protected $richTextAttributes = [
        'body',
        'notes',
    ];
}

此特性将为 Post 模型创建 动态关系,每个字段一个。这些关系将被称为 richText{FieldName},您可以使用下划线定义字段,因此如果您有一个 internal_notes 字段,则将在模型上添加一个 richTextInternalNotes 关系。

为了获得更好的 DX,此特性还将为 Post 模型上的 bodynotes 字段添加自定义类型转换,以便将设置/获取操作转发到关系,因为这些字段将不会存储在帖子表中。这意味着您可以使用 Post 模型如下

$post = Post::create(['body' => $body, 'notes' => $notes]);

并且您可以像与 Post 模型上的任何常规字段一样与丰富文本字段交互

$post->body->render();

再次强调,Post 模型上没有 bodynotes 字段,这些 虚拟字段 将将交互转发到该字段的关联关系。这意味着,当你与这些字段交互时,实际上是在与 RichText 模型的一个实例交互。该模型将有一个包含丰富文本内容的 body 字段。然后,这个字段被转换为 Content 类的一个实例。对 RichText 模型的调用将被转发到 RichText 模型上的 body 字段,它是一个 Content 类的实例。这意味着,而不是

$post->body->body->attachments();

第一个 "body" 是虚拟字段,它将是 RichText 模型的一个实例,第二个 "body" 是该模型上的丰富文本内容字段,它是一个 Content 类的实例,你可以这样做

$post->body->attachments();

类似于 Content 类,RichText 模型将实现 __toString 魔术方法,并通过将其转换为字符串来渲染 HTML 内容(供最终用户使用),在 blade 中可以这样操作

{!! $post->body !!}

注意:由于 HTML 输出不会被转义,请在渲染之前确保对其进行清理。有关更多信息,请参阅 清理 部分。

HasRichText 特性还会添加一个作用域,您可以使用它来预加载丰富文本字段(请记住,每个字段都有自己的关联关系),您可以使用如下方式

// Loads all rich text fields (1 query for each field, since each has its own relationship)
Post::withRichText()->get();

// Loads only a specific field:
Post::withRichText('body')->get();

// Loads some specific fields (but not all):
Post::withRichText(['body', 'notes'])->get();

此示例的数据库结构可能如下所示

posts
    id (primary key)
    created_at (timestamp)
    updated_at (timestamp)

rich_texts
    id (primary key)
    field (string)
    body (long text)
    record_type (string)
    record_id (unsigned big int)
    created_at (timestamp)
    updated_at (timestamp)

我们在 rich_texts 表中存储字段的反向引用,因为一个模型可能有多个丰富文本字段,所以这是在 HasRichText 为您创建的动态关系中使用。此外,该表还有一个唯一约束,这可以防止对同一模型/字段对有多个条目。

将丰富文本内容渲染回 Trix 编辑器的操作与渲染给最终用户略有不同,因此您可以使用字段的 toTrixHtml 方法来完成,如下所示

<x-trix-input id="post_body" name="body" value="{!! $post->body->toTrixHtml() !!}" />

接下来,转到 附件 部分以了解更多关于可附加内容的信息。

加密丰富文本属性

如果您想在存储时加密 HTML 内容,可以在 richTextAttributes 属性中将 encrypted 选项指定为 true

use Tonysm\RichTextLaravel\Models\Traits\HasRichText;

class Post extends Model
{
    use HasRichText;

    protected $guarded = [];

    protected $richTextAttributes = [
        'body' => ['encrypted' => true], // This will be encrypted...
        'notes', // Not encrypted...
    ];
}

这使用了 Laravel 的加密 功能。默认情况下,我们将使用 Laravel 的 Crypt::encryptString() 进行加密,并使用 Crypt::decryptString() 进行解密。如果您是从 Rich Text Laravel 包的 2 版本迁移过来,默认会使用 Crypt::encrypt()Crypt::decrypt(),您必须手动迁移您的数据(请参阅 2.2.0 版本的说明)。这是升级到 3.x 的推荐方法。

话虽如此,您可以通过在 AppServiceProvider::boot 方法中调用 RichTextLaravel::encryptUsing() 方法来配置包如何处理加密。此方法接收加密和解密处理程序。处理程序将接收要加密的值、模型和键(字段),如下所示

namespace App\Providers;

use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\ServiceProvider;
use Tonysm\RichTextLaravel\RichTextLaravel;

class AppServiceProvider extends ServiceProvider
{
    // ...
    public function boot(): void
    {
        RichTextLaravel::encryptUsing(
            encryption: fn ($value, $model, $key) => Crypt::encrypt($value),
            decryption: fn ($value, $model, $key) => Crypt::decrypt($value),
        );
    }
}

再次强调,建议您迁移现有的加密数据并使用默认的加密处理程序(请参阅 此处 的说明)。

密钥轮换

Laravel 的加密组件依赖于 APP_KEY 主密钥。如果您需要轮换此密钥,您需要手动使用新密钥重新加密加密的丰富文本属性。

此外,存储的内容附件依赖于Globalid Laravel包。该包基于您的APP_KEY生成一个派生密钥。当旋转APP_KEY时,您还需要更新所有存储内容附件的sgid属性。

AsRichTextContent特性

如果您不想使用推荐的架构(无论是由于您在这里有强烈的观点,还是您想控制自己的数据库结构),您可以选择跳过整个推荐数据库结构,并使用自定义转换AsRichTextContent在您的富文本内容字段上。例如,如果您在posts表上存储body字段,您可以这样做

use Tonysm\RichTextLaravel\Casts\AsRichTextContent;

class Post extends Model
{
    protected $casts = [
        'body' => AsRichTextContent::class,
    ];
}

然后自定义转换将解析HTML内容并将其压缩以进行存储。本质上,它将转换由Trix提交的内容,该内容仅包含一个图像附件

$post->update([
    'content' => <<<HTML
    <h1>Hello World</h1>
    <figure data-trix-attachment='{
        "url": "http://example.com/blue.jpg",
        "width": 300,
        "height": 150,
        "contentType": "image/jpeg",
        "caption": "Something cool",
        "filename":"blue.png",
        "filesize":1168
    }'>
        <img src="http://example.com/blue.jpg" width="300" height="150" />
        <caption>
            Something cool
        </caption>
    </figure>
    HTML,
])

成为这个压缩版本

<h1>Hello World</h1>
<rich-text-attachment content-type="image/jpeg" filename="blue.png" filesize="1168" height="300" href="http://example.com/blue.jpg" url="http://example.com/blue.jpg" width="300" caption="testing this caption" presentation="gallery"></rich-text-attachment>

再次渲染时,它将在rich-text-attachment标签内重新渲染远程图像。您可以通过简单地回显输出来渲染内容以进行查看,如下所示

{!! $post->content !!}

注意:由于 HTML 输出不会被转义,请在渲染之前确保对其进行清理。有关更多信息,请参阅 清理 部分。

当再次为Trix编辑器提供内容时,您需要这样做

<x-trix-input id="post_body" name="body" value="{!! $post->body->toTrixHtml() !!}" />

编辑器的渲染方式略有不同,因此必须这样。

图像上传

Trix显示了附件按钮,但默认情况下它不起作用,我们必须在我们的应用程序中实现此行为。

附件上传的基本版本可能看起来像这样

  • 监听Trix元素(或任何父元素,因为它会冒泡)上的trix-attachment-add事件;
  • 实现上传请求。在此事件中,您可以访问Trix附件实例,因此如果您想的话,可以更新它上的进度,但这不是必需的;
  • 一旦上传完成,您必须从上传端点返回attachmentURL,您可以使用它来设置附件上的urlhref属性。就是这样。

该包包含一个包含在Workbench应用程序中实现的基本图像上传功能的应用程序示例。以下是相关链接

然而,您并不限于在Trix中进行这种基本的附件处理。更高级的附件行为可以创建自己的后端模型,然后在附件上设置sgid属性,这会让您在文档在Trix编辑器外渲染时完全控制渲染的HTML。

内容附件

使用Trix,我们可以拥有内容附件。为了解决这个问题,让我们在Trix之上构建一个用户提及功能。有一个很好的Rails Conf演讲,构建了整个功能,但使用Rails。工作流程在Laravel中基本上是相同的。

要将任何模型转换为可附加的,您必须实现 AttachableContract。您可以使用 Attachable 特性来提供一些基本的可附加功能(它实现了大多数可附加的基本处理),除了必须实现的 richTextRender(array $options): string 方法。此方法用于确定如何在 Trix 内部和外部渲染内容附加。

传递给 richTextRender$options 数组存在于您在画廊中渲染多个模型的情况下,因此在这种情况下,您将获得一个可选的 in_gallery 布尔字段(对于此用户提及示例不适用,因此我们可以忽略它)。

您可以使用 Blade 来渲染可附加的 HTML 部分内容。作为一个参考,Workbench 应用程序提供了一个用户提及功能,该功能可以作为内容附加的示例。以下是一些相关链接

  • 实现了 AttachmentContract 的用户模型可以在 用户模型 中找到;
  • 该模型使用一个名为 Mentionee 的自定义特性,它底层使用了 Attachable 特性,因此请查看 Mentionee 特性
  • 在前端,我们使用 Zurb 的 Tribute 库在用户在 Trix 中输入 @ 符号时检测提及。设置它的 Simulus 控制器可以在 resources/views/components/app-layout.blade.php 中找到。查找“rich-text-mentions”控制器。这与前面提到的 RailsConf 谈话中提到的实现相同,因此如果您需要了解正在发生的事情,请查看该内容。Workbench 应用程序中有两个 Trix 组件,一个用于帖子评论,可以在 resources/views/components/trix-input.blade.php 中找到,另一个用于聊天编辑器,可以在 resources/views/chat/partials/trix-input.blade.php 中找到。在这两个组件中,您将找到一个监听 tribute-replaced 事件的 data-action 条目,这是 Tribute 为我们派发的创建 Trix 附加的事件,提供用户从下拉菜单中选择的选项;
  • 提及类将在 GET /mentions?search= 路由中查找提及,您可以在 routes/web.php 中找到它。注意,我们将 sgidcontent 字段转换为 Trix 附加,返回的 name 字段也用于 Tribute 本身来构建提及功能。
  • 渲染用户附加的 Blade 视图可以在 resources/views/mentions/partials/user.blade.php 中找到

您可以在稍后从该富文本内容检索所有附加内容。有关更多信息,请参阅 内容对象 部分。

内容对象

您可能希望在稍后检索富文本内容中的所有可附加对象,并对其进行一些花哨的处理,例如将与帖子模型关联的用户提及实际存储起来。或者,您可以检索该富文本内容中的所有链接并对其进行处理。

获取附件

您可以使用 attachments() 方法在 RichText 模型实例或内容实例中检索富内容字段的全部附件。

$post->body->attachments()

这将返回所有附件的集合,实际上是指所有可附件的内容,例如图像和用户。如果您只想获取特定附件的内容,您可以使用集合上的过滤方法,如下所示

// Getting only attachments of users inside the rich text content.
$post->body->attachments()
    ->filter(fn (Attachment $attachment) => $attachment->attachable instanceof User)
    ->map(fn (Attachment $attachment) => $attachment->attachable)
    ->unique();

获取链接

要从富文本内容中提取链接,您可以调用links()方法,如下所示

$post->body->links()

获取附件画廊

Trix有一个画廊的概念,您可能想要检索所有画廊

$post->body->attachmentGalleries()

这将返回所有图像画廊的DOMElement集合。

获取画廊附件

您可能还想获取图像画廊内的所有附件。您可以通过以下方式实现

$post->body->galleryAttachments()

这将返回一个包含画廊中所有图像附件的集合(所有附件)。然后您可以通过以下方式检索RemoteImage附件实例

$post->body->galleryAttachments()
    ->map(fn (Attachment $attachment) => $attachment->attachable)

无SGID的自定义内容附件

您可能想要附加不需要存储在数据库中的资源。一个例子可能是将聊天消息中的OpenGraph嵌入存储。您可能不想将每个OpenGraph嵌入作为一个独立的数据库记录存储。对于这类数据完整性并非关键的情况,您可以注册一个自定义附件解析器

use App\Models\Opengraph\OpengraphEmbed;
use Illuminate\Support\ServiceProvider;
use Tonysm\RichTextLaravel\RichTextLaravel;

class AppServiceProvider extends ServiceProvider
{
    public function boot()
    {
        RichTextLaravel::withCustomAttachables(function (DOMElement $node) {
            if ($attachable = OpengraphEmbed::fromNode($node)) {
                return $attachable;
            }
        });
    }
}

此解析器必须返回一个AttachableContract实现实例或如果节点不匹配您的附件,则返回null。在OpengraphEmbed的这种情况下,它可能看起来像这样

namespace App\Models\Opengraph;

use DOMElement;
use Tonysm\RichTextLaravel\Attachables\AttachableContract;

class OpengraphEmbed implements AttachableContract
{
    const CONTENT_TYPE = 'application/vnd.rich-text-laravel.opengraph-embed';

    public static function fromNode(DOMElement $node): ?OpengraphEmbed
    {
        if ($node->hasAttribute('content-type') && $node->getAttribute('content-type') === static::CONTENT_TYPE) {
            return new OpengraphEmbed(...static::attributesFromNode($node));
        }

        return null;
    }

    // ...
}

您可以在聊天工作台演示(或在这个PR中)看到这个OpenGraph示例的完整工作实现。

纯文本渲染

Trix内容可以被转换为任何东西。这本质上意味着HTML > something。该软件包自带一个HTML > Plain Text实现,因此您可以通过在Trix内容上调用toPlainText()方法将其转换为纯文本

$post->body->toPlainText()

例如,以下富文本内容

<h1>Very Important Message<h1>
<p>This is an important message, with the following items:</p>
<ol>
    <li>first item</li>
    <li>second item</li>
</ol>
<p>And here's an image:</p>
<rich-text-attachment content-type="image/jpeg" filename="blue.png" filesize="1168" height="300" href="http://example.com/blue.jpg" url="http://example.com/blue.jpg" width="300" caption="The caption of the image" presentation="gallery"></rich-text-attachment>
<br><br>
<p>With a famous quote</p>
<blockquote>Lorem Ipsum Dolor - Lorense Ipsus</blockquote>
<p>Cheers,</p>

将被转换为

Very Important Message

This is an important message, with the following items:

    1. first item
    1. second item

And here's an image:

[The caption of the image]

With a famous quote

“Lorem Ipsum Dolor - Lorense Ipsus”

Cheers,

如果您正在附加模型,您可以在其上实现richTextAsPlainText(?string $caption = null): string方法,其中您应该返回该附件的纯文本表示。如果该附件上没有实现此方法,并且Trix附件中没有存储标题,则该附件将不会出现在内容的纯文本版本中。

清理

由于我们正在渲染用户生成的HTML,您必须对其进行清理以避免任何安全问题。即使我们控制输入元素,恶意用户也可能在浏览器中篡改HTML并将其替换为允许他们注入自己的HTML的其他内容。

我们建议使用Symfony的HTML清理器。该存储库中的Workbench应用程序自带一个示例实现。以下是一些相关信息

  • 必须始终对由软件包生成的HTML和纯文本版本的HTML进行转义。永远不要信任用户生成的内容。
  • 一个转义内容的例子可以在resources/views/posts/show.blade.php中找到。请注意,富文本属性正在传递给clean()函数;
  • clean() 函数创建了一个Sanitizer(参见工厂),它是基于Symfony的HTML Sanitizer(参见Sanitizer)的一个轻量级抽象;
  • 在Workbench应用的所有示例中,我们只在对内容进行渲染时进行清理。您也可以考虑在验证之后对其进行清理,甚至在将其传递到模型之前。

注意:我不是HTML内容清理的专家,所以请多加小心,如果可能的话,请咨询在这方面有更多安全经验的某人。

SGID

当存储自定义附件的引用时,该软件包使用另一个名为GlobalID Laravel的软件包。我们存储一个签名全局ID,这意味着用户无法简单地更改静止状态下的sgids。他们需要使用秘密的APP_KEY生成另一个有效的签名。

如果您想更换密钥,您需要遍历所有富文本内容,找到所有具有sgid属性的附件,使用新的密钥和新的签名给它分配一个新值,并使用该新值存储内容。

Livewire

如果您想使用Livewire与Trix和Rich Text Laravel集成,最佳方式是使用Livewire的@entangle()功能。Workbench应用附带一个示例应用。一些有趣的点:

  • 有一个自定义的components/trix-input-livewire.blade.php,仅用于展示如何与Livewire一起使用;
  • 如你所见,它依赖于entangle。这是推荐的方式;
  • 查看Livewire\Posts组件。当用户点击“编辑”时,它会将当前正在编辑的帖子设置为状态,并使用帖子模型中的数据填充PostForm,包括Trix HTML;

测试

composer test

更新日志

有关最近更改的更多信息,请参阅更新日志

贡献

有关详细信息,请参阅贡献指南

安全漏洞

请参阅我们的安全政策以了解如何报告安全漏洞。

致谢

许可

MIT许可(MIT)。有关更多信息,请参阅许可文件