一、先聊聊文档输出这件事

平时写文档,我们大多数人都是用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调一整天字体来得痛快。