Rails Action Text 更新解读:to_markdown 转换、编辑器扩展机制与 Trix 弃用路径
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::MarkdownConversion 的 node_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 的示例:
- RemoteImage 的
attachable_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_text(content.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::Editor 与 TrixEditor。
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? 要求 image 或 image/*。两个条件都通过才构造对象,否则 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::JsPackageManager,package_add_command 会根据应用现状拼出正确的包安装命令(如 pnpm add / yarn add / bin/importmap pin),而不再硬编码某一种工具。此外 append_editor 与 append_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
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 加入序列化属性列表。
小结
本轮变更的主线是富文本内容的多格式消费能力与编辑器抽象的解耦:
to_markdown让 Action Text 内容能以与to_plain_text对称的方式导出为 Markdown,并内置了文本转义、URL 校验与保留元素拆包三道安全边界;MissingAttachable的 "☒" 统一了纯文本、Markdown、HTML 三种表示下"附件缺失"的语义;- block 与 value 的独立语义为 Lexxy 等新编辑器打开了通过 DOM 子元素注入配置的通道,同时 Trix 的旧契约在
TrixEditor::Tag#render_in中保持不变; RemoteImage的创建期校验把渲染期错误前移为解析期降级,提升了含损坏内容页面的健壮性;- 安装生成器适配多种 JS 包管理器,降低了接入成本;
- Trix 命名 API 的批量弃用,标志着 Action Text 从"绑定 Trix"走向"编辑器无关"的架构方向。
延伸阅读可参考 actiontext/README.md 与 指南文档,附件与编辑器的实现细节分别在 lib/action_text/attachment.rb、lib/action_text/editor/registry.rb,测试用例集中在 actiontext/test/unit/ 目录。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00