首页
/ Rails Action Text 更新解读:to_markdown 转换、编辑器扩展机制与 Trix 弃用路径

Rails Action Text 更新解读:to_markdown 转换、编辑器扩展机制与 Trix 弃用路径

2026-09-06 19:40:02作者:江焘钦

Action Text 是 Ruby on Rails 中负责富文本内容处理的核心组件,它把带附件的 HTML 片段封装成可解析、可序列化、可渲染的对象。本文基于当前仓库 actiontext/CHANGELOG.md 的变更记录展开,逐项解读本轮更新:to_markdown 富文本转 Markdown 转换、MissingAttachable 纯文本渲染、编辑器块级子元素语义变更、RemoteImage URL 创建期校验、安装生成器的 JS 包管理器自动检测,以及一批 Trix 专属 API 的弃用声明。读完后,你将了解这些新 API 的实际用法、参数细节,以及它们在源码中的实现路径与验证方式,从而在生产 Rails 应用中安全地消费 Action Text 富文本内容。

变更记录总览

本轮 Action Text 变更可归纳为三类:

  • 能力新增Content#to_markdown(富文本转 Markdown)、安装生成器自动检测 JS 包管理器、编辑器 block 语义支持 value 与 block 并存;
  • 行为修正MissingAttachable 纯文本渲染从空白改为 "☒"、RemoteImage 在创建期校验 URL;
  • API 演进:一批 Trix 专属类与方法被标记弃用,ActionText::Attachable#read_attribute_for_serialization 改为 public。

下面按主题逐项深入。

to_markdown:与 to_plain_text 对称的 Markdown 转换

变更说明:新增 ActionText::Content#to_markdown,支持标题、加粗、斜体、删除线、行内代码、代码块、引用、有序/无序列表、链接、表格和附件;自定义附件的 Markdown 表示可通过在 attachable 模型上实现 attachable_markdown_representation 提供:

message = Message.create!(content: "<h1>Hello</h1><p>This is <strong>bold</strong></p>")
message.content.to_markdown # => "# Hello\n\nThis is **bold**"

源码实现路径

to_markdown 定义在 Content#to_markdown

def to_markdown(attachment_links: false)
  render_attachments(with_full_attributes: false) { |attachment|
    ActionText::MarkdownConversion.render_attachment(attachment, attachment_links: attachment_links)
  }.fragment.to_markdown
end

调用链为:Content#to_markdown → 先经 render_attachments 把每个附件节点替换为附件的 Markdown 片段 → 交给 ActionText::MarkdownConversionnode_to_markdown 做自底向上的 HTML → Markdown 归约(基于 ActionText::BottomUpReducer)。转换器对每种节点使用 visit_ 前缀的方法分派:

  • 标题 h1~h6# 前缀(visit_h1);
  • <strong>/<b><em>/<i>** / * 包裹,且对 Lexxy 编辑器冗余嵌套的 <b> 做了去重(visit_strong);
  • <s>/<del>~~ 删除线;
  • <pre> → 围栏代码块,且围栏长度会自适应内容中反引号的最大连续长度,避免内容本身破坏围栏(code_fence);
  • <a>title,链接目标会做百分号编码,且 scheme 需通过 Rails::HTML::Sanitizer.allowed_uri? 校验,不允许的 URL(如 javascript:)会退化为转义后的纯文本(markdown_link);
  • 表格行 → | a | b | 语法,全为 <th> 的行被识别为表头并自动补分隔行(visit_tr)。

文本转义与防注入

转换器会转义所有普通文本节点中的 CommonMark 元字符(MARKDOWN_METACHARACTERS),因此 <p># Release Notes</p> 产出的是字面文本 \# Release Notes 而非标题——这一点在 markdown_conversion.rb 头部注释 中明确说明。

关于 <action-text-markdown> 保留元素:变更说明指出该元素为 Action Text 内部预留,会被从富文本内容中移除。具体机制在 Content.fragment_by_canonicalizing_content 中——内容规范化时调用 MarkdownConversion.fragment_by_unwrapping_raw_markdown_tags,把该元素替换为其子节点,使内部的 Markdown 源文本像普通文字一样被转义。而 #render_attachment 恰恰利用该元素把已渲染的附件 Markdown "原样透传"(render_attachment):只有 Action Text 框架内部会生成该元素,用户在内容中手写它时会在规范化阶段被拆掉。

附件的自定义 Markdown 表示

attachment_links 参数为 true 时,ActiveStorage blob 附件会生成带 URL 的 Markdown 链接;源码注释指出这需要渲染上下文(如控制器或 mailer 动作),URL 生成失败时会抛错(content.rb#L144-L146)。

内置 attachable 的示例:

  • RemoteImageattachable_markdown_representation 产出图片链接 caption
  • MissingAttachable 的表示就是下文提到的 "☒"。

自定义模型只需实现:

class Person < ApplicationRecord
  include ActionText::Attachable

  def attachable_markdown_representation(caption, attachment_links: false)
    "[Person: #{name}]"
  end
end

与 to_plain_text 的对称性

to_plain_textcontent.rb#L132-L134)与 to_markdown 走同一套 render_attachments 流程,只是最终分别调用 fragment 的 to_plain_text / to_markdown。两者返回值都不是 HTML safe 的,源码注释明确提醒不能直接渲染到浏览器,需额外消毒(content.rb#L126-L131)。相关测试位于 actiontext/test/unit/content_test.rb

MissingAttachable 纯文本渲染改为 "☒"

变更说明:此前 Content#to_plain_text 遇到 MissingAttachable(即 sgid 指向的记录已被删除或不可解析)会替换为空白字符串;现在与 HTML 表示保持一致,渲染为 "☒" 字符。

源码印证在 MissingAttachable

def attachable_plain_text_representation(caption = nil)
  "☒"
end

def attachable_markdown_representation(caption = nil, attachment_links: false)
  "☒"
end

纯文本与 Markdown 两个通道现在行为一致。这意味着导出富文本为纯文本(例如邮件正文、导出 CSV)时,丢失的附件不再表现为"莫名空缺",而是有一个明确的占位标记,便于人工识别内容缺损。MissingAttachable 的产生路径见 Attachable.from_node:sgid 定位失败且非内容附件、非远程图片时,兜底返回 MissingAttachable.new(node["sgid"])

编辑器 block 子元素与 value 并存

变更说明:block 参数最初在 #55827 中引入时,仅作为 value 参数的替代——block 被捕获后当作编辑器初始内容,二者只能取其一。本次变更把语义调整为:block 渲染为编辑器元素的 DOM 子节点,与 value 相互独立。value 流入编辑器的内容绑定(Trix 是隐藏 input,自定义编辑器是 value 属性),block 则作为内部 DOM 子元素——适合嵌入自定义元素,例如提示词菜单或工具栏扩展。这使得 Lexxy 等编辑器可以通过 block 形式做配置(向编辑器标签注入子元素),同时富文本 value 被单独保留。

Trix 保持原有"block 作为初始值"的契约:当没有 value 时在 TrixEditor::Tag#render_in 中捕获 block 作为隐藏 input 的值。源码印证在 TrixEditor::Tag#render_in

def render_in(view_context, ...)
  name = options.delete(:name)
  form = options.delete(:form)
  value = options.delete(:value)
  value = view_context.capture(&@block) if @block && value.nil?
  # ...
  input_tag = view_context.hidden_field_tag(name, value, id: options[:input], form: form)
  input_tag + view_context.content_tag(element_name, nil, options)
end

关键行是 value = view_context.capture(&@block) if @block && value.nil?:只有 value 缺省时 block 才退化为初始值,否则 block 由外层标签机制正常渲染为子内容。编辑器整体抽象见 ActionText::EditorTrixEditor

RemoteImage 创建期 URL 校验

变更说明:RemoteImage.from_node 现在在创建 RemoteImage 对象之前先用 AssetUrlHelper 渲染时所用的同一正则校验 URL。像 "image.png" 这类过去会流进 asset pipeline 并抛出 ActionView::Template::Error 的相对路径,现在会在早期被拒绝,优雅地降级为 MissingAttachable

源码印证在 RemoteImage.from_node

def from_node(node)
  if remote_url?(node["url"]) && content_type_is_image?(node["content-type"])
    new(attributes_from_node(node))
  end
end

其中 remote_url?ActionView::Helpers::AssetUrlHelper::URI_REGEXP.match?(url) 判断(remote_image.rb#L18-L20),content_type_is_image? 要求 imageimage/*。两个条件都通过才构造对象,否则 Attachable.from_node 的兜底分支返回 MissingAttachable——这正是"gracefully fail"的机制。这个改进把原本只在渲染期才暴露的 asset 解析错误提前到解析期,并且不再向上抛 Template::Error,降低了含损坏富文本的页面整体渲染失败的风险。

安装生成器自动检测 JS 包管理器

变更说明:rails action_text:install 安装编辑器 JS 依赖时,现在会自动检测应用使用的 JS 包管理器(如 importmap、Bun、pnpm、Yarn、npm)。

源码印证在 InstallGenerator

include Rails::Generators::JsPackageManager

class_option :editor, type: :string, default: "trix"

def install_editor
  return unless using_js_runtime?
  run package_add_command(options[:editor])
end

def install_javascript_dependencies
  return unless using_js_runtime?
  run package_add_command("@rails/actiontext")
end

生成器混入了 Rails::Generators::JsPackageManagerpackage_add_command 会根据应用现状拼出正确的包安装命令(如 pnpm add / yarn add / bin/importmap pin),而不再硬编码某一种工具。此外 append_editorappend_javascript_dependencies 会自动处理两个入口:

  • 若存在 app/javascript/application.js,插入 import "trix" / import "@rails/actiontext"
  • 若存在 config/importmap.rb,追加 pin "trix"pin "@rails/actiontext", to: "actiontext.esm.js"

create_migrations 则通过 railties:install:migrations FROM=active_storage,action_text 把 Active Storage 与 Action Text 的迁移一并拷入应用(install_generator.rb#L78-L80)。--editor 选项默认 trix,为 Lexxy 等其他编辑器保留了接入点。

Trix 专属 API 弃用与编辑器抽象演进

变更说明宣布弃用一批 Trix 专属的类、模块与方法,统一指向编辑器无关的替代 API:

弃用项 替代
ActionText::Attachable#to_trix_content_attachment_partial_path 覆写 #to_editor_content_attachment_partial_path
ActionText::Attachments::TrixConversion (编辑器层抽象,见 ActionText::Editor
ActionText::Content#to_trix_html to_editor_html
ActionText::RichText#to_trix_html to_editor_html
ActionText::TrixAttachment 编辑器内部实现细节

源码中可以看到弃用声明的实际形态,例如 Attachable#to_trix_content_attachment_partial_path

def to_trix_content_attachment_partial_path
  to_editor_content_attachment_partial_path
end
deprecate to_trix_content_attachment_partial_path: :to_editor_content_attachment_partial_path, deprecator: ActionText.deprecator

以及 Content#to_trix_html

def to_trix_html
  to_editor_html
end
deprecate :to_trix_html, deprecator: ActionText.deprecator

旧方法仍会委托到新方法(保证向后兼容),同时通过 ActionText.deprecator 发出弃用警告。迁移建议:

  • 自定义 attachable 中覆写 to_editor_content_attachment_partial_path 而非 to_trix_content_attachment_partial_path
  • 视图层获取编辑器可编辑 HTML 时用 to_editor_html
  • 关注弃用输出(默认写日志),在下一个主版本前完成替换。

read_attribute_for_serialization 改为 public

变更说明:ActionText::Attachable#read_attribute_for_serialization 现在是 public 方法。源码中它负责在序列化时暴露 attachable_sgid:未持久化的记录返回 nil,避免泄漏本地对象的 sgid(attachable.rb#L158-L170):

# See ActiveModel::Serialization#read_attribute_for_serialization.
def read_attribute_for_serialization(key)
  if key == "attachable_sgid"
    persisted? ? super : nil
  else
    super
  end
end

private
  def attribute_names_for_serialization
    super + ["attachable_sgid"]
  end

改为 public 的意义在于:ActiveModel 序列化契约中该方法是外部可调度的接口,此前为 private 会在部分自定义序列化场景(如覆写或代理包装 attachable 模型)中造成反射调用失败。私有保留的 attribute_names_for_serialization 则继续负责把 attachable_sgid 加入序列化属性列表。

小结

本轮变更的主线是富文本内容的多格式消费能力编辑器抽象的解耦

  1. to_markdown 让 Action Text 内容能以与 to_plain_text 对称的方式导出为 Markdown,并内置了文本转义、URL 校验与保留元素拆包三道安全边界;
  2. MissingAttachable 的 "☒" 统一了纯文本、Markdown、HTML 三种表示下"附件缺失"的语义;
  3. block 与 value 的独立语义为 Lexxy 等新编辑器打开了通过 DOM 子元素注入配置的通道,同时 Trix 的旧契约在 TrixEditor::Tag#render_in 中保持不变;
  4. RemoteImage 的创建期校验把渲染期错误前移为解析期降级,提升了含损坏内容页面的健壮性;
  5. 安装生成器适配多种 JS 包管理器,降低了接入成本;
  6. Trix 命名 API 的批量弃用,标志着 Action Text 从"绑定 Trix"走向"编辑器无关"的架构方向。

延伸阅读可参考 actiontext/README.md指南文档,附件与编辑器的实现细节分别在 lib/action_text/attachment.rblib/action_text/editor/registry.rb,测试用例集中在 actiontext/test/unit/ 目录。

登录后查看全文
热门项目推荐
相关项目推荐