一、先聊聊文档输出这件事
平时写文档,我们大多数人都是用Word或者Markdown。Word排版起来挺麻烦,得手动调字体、缩进、页眉页脚;Markdown虽然轻快,但遇到稍微复杂一点的排版需求,比如“这一块要变成好看的警告框”、“这里要自动生成个表格”,就容易束手无策。
后来我发现了一套叫AsciiDoc的描述语言。它跟Markdown很像,但天生给“结构化文档”准备的。你可以把一本书的内容全部写在纯文本里,然后用一个叫Asciidoctor的工具,把这堆纯文本转成网页、PDF、电子书。最棒的是,Asciidoctor不是个死板的工具,它提供了一套插件机制,让你能按照自己的口味,定制输出的每一个细节。
这一篇我们就来聊聊,怎么利用Asciidoctor的插件扩展,让同样的AsciiDoc源文档,输出成完全符合我们需要的样式。
1.1 AsciiDoc和Asciidoctor是啥关系
简单打个比方:AsciiDoc是“菜谱”,Asciidoctor是“炒菜的锅”。菜谱规定了食材怎么切、调料怎么放;锅把菜谱变成一桌热腾腾的菜。Asciidoctor读取AsciiDoc文件,然后生成HTML或PDF。这里面的转换规则,很多都是内置的。但就像我们可以往锅里加辅助工具一样,我们也可以在Asciidoctor里装“扩展”,让它理解新的语法,或者把旧的语法按新方式呈现。
1.2 为啥要折腾插件
有人会问:“既然Asciidoctor已经很能打,我干嘛还要写插件?”举个例子:公司内部的文档要求所有“危险操作”提示都必须是红色边框、黄色背景,而且左上角要有“注意”字样。默认的Asciidoctor输出的是简单的“Note”块,风格可能跟公司品牌不一致。怎么办?你可以自己写一个插件,把文档里的 [warning] 块变成公司规定的样式。这样你的文档源文件依然好读,输出却“服服帖帖”。
二、Asciidoctor插件到底在干嘛
插件其实就是一个Ruby类。Asciidoctor在转换文档的各个阶段会触发不同的事件,你的插件就躲在某个事件里,偷偷修改文档树或者输出结果。最常用的三种插件类型是:
- 块处理器:对付文档里的一个块,比如段落、表格、代码块。
- 行内宏处理器:对付一行里面的小标签,比如把
email:变成一个超链接。 - 树处理器:等整篇文档解析完成,把所有内容看成一棵树,你再整体修修剪剪。
这个思路就像给文档加“滤镜”。你写文档的时候只关心内容,最后的呈现方式交给滤镜去处理。
2.1 块处理器的基本套路
块处理器的开发套路比较固定。你得先写一个类,继承某个基类,然后用语言里的小技巧告诉Asciidoctor:“嘿,凡是叫 foo 的块,都归我管。”接下来,当Asciidoctor遇到这种块时,它会把你指定的内容交给你的类处理。你写完处理后,再把结果放回文档树。最后渲染输出,你的定制效果就出现了。
三、动手写一个“注意”提示框插件
光说不练假把式,我们直接写一个真正能用的插件。
3.1 准备开发环境
你的电脑上得先装Ruby和Asciidoctor。打开终端,执行:
# 技术栈:Ruby 2.7 (gem命令用于安装Ruby包)
gem install asciidoctor
就这么简单。我们下面所有的代码都基于Ruby 2.7,技术栈非常干净。
3.2 编写插件代码
我们希望在文档里这样写:
[warning]
====
这里要小心,千万不要把咖啡洒在键盘上。
====
然后插件把它输出成一个黄底红边的警告框。来看完整代码:
# 技术栈:Ruby 2.7
# 使用前请先安装 asciidoctor:gem install asciidoctor
require 'asciidoctor'
require 'asciidoctor/extensions'
# 自定义块处理器:名字叫 warning
class WarningBlock < Asciidoctor::Extensions::BlockProcessor
use_dsl
# 让Asciidoctor认识 [warning] 这个块名
named :warning
# 这个块在语料里可以放在普通段落或者示例块里
on_context :paragraph, :example
# 把块里的内容当普通文本读进来,不做复杂嵌套解析
parse_content_as :simple
def process(parent, reader, attrs)
# reader.lines 拿到的是一行一行文本,我们把它拼成一句话
text = reader.lines.join("\n")
# 构造一个带着内联样式的HTML代码段
html = %(
<div style="border:2px solid #e67e22; background:#fdf2e9;
padding:14px; margin:14px 0; border-radius:6px;">
<strong>⚠ 注意:</strong> #{text}
</div>
)
# create_block 创建一个“原样输出”的块,HTML会直接进入最终结果
create_block parent, :pass, html, attrs
end
end
# 把插件注册进Asciidoctor
Asciidoctor::Extensions.register do
block WarningBlock
end
# 下面是一小段待转换的AsciiDoc文档,我们把它放在Ruby字符串里
# 这样整个示例就只用了一种技术栈,方便直接运行
adoc_input = <<~'ADOC'
= 示例文档
这是一段最普通的正文。
[warning]
====
请在执行删除命令之前,再次确认整个目录名称。
====
(完)
ADOC
# 执行转换,输出HTML
puts Asciidoctor.convert(adoc_input, backend: 'html5')
运行这段Ruby脚本,你会在终端看到输出的HTML。里面出现了一个带橙色边框的div,里面包着“注意”两个字和你的原文。不管你用什么浏览器打开这个HTML,都能看到醒目的提示框。
3.3 代码里几个关键点
named :warning是给这个插件起了个名字,这样文档里的[warning]才会被它接住。on_context表示这个块可以出现在什么场景里。这里写了段落和示例块。process方法是真正的处理逻辑。parent相当于当前文档树的“父亲”,你要把自己生成的新节点挂到它下面。create_block parent, :pass, html, attrs的意思是:在父亲节点下创建一个“pass”块。pass块就是“原样通过”的意思,里面的HTML不会被转义,直接输出。
3.4 如果不想用内联样式呢
上面为了演示方便,把样式直接写在HTML标签里。在实际项目中,更好的做法是给div一个类名,然后在CSS文件里统一写样式。这样文档更多变,维护也方便。你只需要把html字符串改成:
html = %(<div class="custom-warning">#{text}</div>)
然后在你的网站样式表里写 .custom-warning { ... } 就行。Asciidoctor插件只是负责“摆结构”,外观完全交给CSS,多干净。
4.4 关联技术:CSS 和主题
如果你仔细观察上面的例子,会发现我们只在HTML里写了结构,样式都靠内联CSS。在真实项目中,你应该把Asciidoctor生成的HTML挂到一套完整的CSS样式表里。Asciidoctor本身支持自定义主题,很多静态网站生成器也能和它无缝集成。插件负责“长出骨头”,CSS负责“穿衣服”。这样当公司换Logo换配色时,只需要改一版CSS,所有文档随之焕然一新。
四、再做一个小工具:把CSV变成HTML表格
第二个例子,我们解决一个很常见的麻烦:文档里要放表格,数据在Excel里,手动粘贴进AsciiDoc又得重新排版。我们干脆写一个插件,让它直接解析“用逗号分隔的文本”,然后输出成标准HTML表格。
4.1 插件的目标用法
我们希望以后在文档里这样写:
[pricetable]
====
商品,价格,库存
手机,5999,200
平板,3999,150
耳机,899,300
====
插件会自动生成一个像样的HTML表格。
4.2 完整Ruby实现
# 技术栈:Ruby 2.7
# 需要Ruby自带的csv库,注意CSV模块可以处理引号、转义等复杂情况
require 'csv'
require 'asciidoctor'
require 'asciidoctor/extensions'
# 自定义块处理器:把CSV文本转成HTML表格
class CsvTableBlock < Asciidoctor::Extensions::BlockProcessor
use_dsl
named :pricetable
on_context :example
parse_content_as :simple
def process(parent, reader, attrs)
rows = []
# 逐行解析CSV,跳过空行
reader.lines.each do |line|
next if line.strip.empty?
rows << CSV.parse_line(line.strip)
end
# 第一行作为表格头部
header = rows.shift || []
# 生成表头HTML
head_html = header.map { |h| "<th>#{h}</th>" }.join
# 生成每一行的HTML
body_html = rows.map do |row|
cells = row.map { |c| "<td>#{c}</td>" }.join
"<tr>#{cells}</tr>"
end.join
# 拼装整个表格,带一点点基础样式
table_html = <<~HTML
<table style="border-collapse:collapse; width:100%; margin:16px 0;" border="1">
<thead><tr>#{head_html}</tr></thead>
<tbody>#{body_html}</tbody>
</table>
HTML
# 用pass块把HTML放进文档树
create_block parent, :pass, table_html, attrs
end
end
# 注册
Asciidoctor::Extensions.register do
block CsvTableBlock
end
# 测试文档
doc = <<~'ADOC'
= 产品价格表
[pricetable]
====
商品,价格,库存
手机,5999,200
平板,3999,150
耳机,899,300
====
ADOC
# 转换并打印HTML
puts Asciidoctor.convert(doc, backend: 'html5')
运行之后,生成的HTML里多了一个规整的表格。以后你从Excel里复制数据,粘到这个块里,保存转换,一个漂漂亮亮的表格就出来了。是不是省了不少力气?
4.3 这个例子里的小心思
我们用了Ruby自带的CSV库,而没直接用字符串分割。这是因为CSV格式里可能有引号、换行、逗号转义等奇葩情况。用专业库去解析,能避免很多莫名其妙的bug。另外,我们把HTML生成逻辑全放在Ruby里,意味着只要改一处,所有使用了这个块的文档都会跟着变。这就是“内容与表现分离”的甜头。
五、这类插件能用在哪些实际场景
插件听上去很酷,那到底什么时候用它?我总结几个真实场景。
5.1 技术文档网站
很多开源项目的文档都是用AsciiDoc写的,再用Asciidoctor转成静态网站。这时候你希望所有“提示”块都长一个样,代码“复制按钮”能自动生成,页面侧边栏能根据标题自动收缩。这些统统可以做成插件。写文档的人只管写内容,样式和交互交给插件。
5.2 企业内部规范制度
公司发布操作手册、安全规范,往往有严格的视觉要求。比如“违规操作”必须红底白字,“温馨提醒”必须蓝底。如果每个人复制HTML到Word里改,那准乱套。用Asciidoctor插件统一渲染,不仅格式一致,而且文本仍是干净的AsciiDoc,方便审查和版本管理。
5.3 书稿和课程讲义
写书的人最怕排版。我见过有人用Asciidoctor写整本书,再通过插件把章节首页的图标、页脚的公司logo、甚至“读者练习”的方框全部定制好。源文件稳定,输出每次都一样,不会因为不同版本Word导致乱码。
5.4 数据驱动的文档
像我们刚才写的表格插件,还能继续演进。比如从外部API拉取数据,塞进文档,或是在构建时读取JSON文件,自动生成图表。扩展点非常多,AsciiDoc只是入口,输出千变万化。
六、优缺点和注意事项
任何技术都有两面性,Asciidoctor插件也一样。
6.1 优点
- 输出精准:能完全控制最终HTML里的细节,不像有些工具只能改个标题颜色。
- 可复用:写一次插件,整个团队、整个项目都能用,省心。
- 和源文档解耦:修改样式不用动内容文件,历史版本对比更清晰。
- Ruby生态强大:缺什么库都可以从Gem里找,比如解析JSON、读写Excel,都能插进扩展里。
6.2 缺点
- 有学习成本:你得会一点Ruby。好在Asciidoctor的扩展API设计得比较友好,照猫画虎不算难。
- 维护依赖:Asciidoctor版本更新,个别API可能变化,插件也要跟着升级,需要人盯。
- 调试不直观:有时候生成的HTML不是你想的那样,你得在头脑里模拟文档树,排查问题比普通脚本稍麻烦。
- 可移植性差:写好的插件只能在Asciidoctor用,如果某天换成别的文档工具,这些定制就都废了。
6.3 注意事项
- 能用内置节点就用内置节点,别一上来就拼HTML。内置的表格、段落、列表在转PDF时表现更好。
- 如果非要拼HTML,一定要对用户输入做转义,否则会有XSS风险。比如用户写了
<script>,你的插件直接输出,那是非常危险的。 - 多看文档,官方参考是Asciidoctor Extensions文档。不同版本之间,
create_block的参数顺序可能不太一样,务必以你使用的版本为准。 - 注册扩展的时机要把握好。在Asciidoctor转换开始前注册,别在转换过程中生硬插入。
- 最好给插件写一点自动化测试,保证核心处理逻辑正确。Ruby生态里有minitest、rspec,随便用。
七、总结
Asciidoctor插件扩展就像是在文档世界里的“定制造型师”。它让AsciiDoc不仅仅是纯文本转HTML,而是一套灵活的内容发布系统。你可以按自己的需求,做出警告框、表格、图表、导航菜单,甚至完全独特的页面结构。今天这两个小示例,只是带你打开了一扇门:一个处理块,一个处理数据。如果你心动了,不妨打开终端,装上Asciidoctor,试着写一个你想要的插件。刚开始可能会碰壁,但当你看到自己定制的HTML在浏览器里呈现的那一刻,那种成就感绝对比用Word调一整天字体来得痛快。
评论
围绕“Asciidoctor插件扩展在AsciiDoc文档定制化输出中的应用实践”参与讨论