转换器模板

Asciidoctor 生成的所有输出都是可自定义的。自定义输出的一种方法是使用自定义转换器。转换器模板提供了一种更简单的方法。

内置转换器以及启用supports_templates特性的其他转换器允许您使用模板替换任何可转换上下文的转换处理程序。此机制的目标是更轻松地自定义转换器(例如 HTML)的输出,而无需开发、注册和使用自定义转换器。换句话说,您可以按需自定义转换器的输出,而无需编写代码(除了模板中的内容)。

本页将介绍 Asciidoctor 中的转换器模板机制是如何工作的,以及如何利用它来定制支持它的转换器的输出。

什么是模板?

模板是一种专注于生成输出的源文件类型。当我们在上下文中引用“模板”一词时,我们特指 Ruby 模板引擎的源文件。

您可以将模板视为源代码,其中输出文本构成主要结构,编程逻辑则穿插其中。如果您有编程背景,这就像程序逻辑和字符串被颠倒了。

编写模板时,您以静态文本开始。然后,您在这些文本区域周围插入逻辑。此逻辑允许您根据后端数据模型提供的信息有条件地选择文本或重复文本。这种插入逻辑的语法就是模板语言。

Ruby 中最常见的模板引擎是 ERB,因为它内置于语言中。以下是一个 ERB 模板示例,它使用后端数据模型提供的 content 来输出段落元素。

paragraph.html.erb
<p><%= content %></p>

某些模板语言,如 Slim 和 Haml,结构更复杂。以下是使用 Slim 编写的先前示例:

paragraph.html.slim
p =content

以及 Haml 版本:

paragraph.html.haml
%p =content

请注意,这些模板中没有尖括号。这是因为 Slim 和 Haml 假定每个语句的开头是 XML 元素的名称(Haml 需要前缀 %)。等号告诉 Slim 和 Haml 评估后面的 Ruby 表达式,并将结果插入输出。在这种情况下,模板正在读取 content 变量,该变量技术上是后端数据模型的属性。Slim 和 Haml 使用缩进推断 HTML 结构中的嵌套(类似于 YAML)。

运行时,模板引擎会将模板转换为可执行代码并调用它。我们将此操作称为调用模板。有效地,此操作会调用逻辑并扩展模板中的变量引用以生成解析后的输出。

现在您已经熟悉了模板的基本概念,让我们来看看 Asciidoctor 中模板的使用方式。

Asciidoctor 中的模板

Asciidoctor 使用模板来自定义转换器生成的输出(前提是转换器已启用 supports_templates 特性)。我们将这些称为转换器模板。转换器模板与它们应用的转换器协同工作。

您可以将会为 Asciidoctor 开发的一组模板与 AsciidoctorJ 重复使用,从而使模板在两种运行时之间可移植。Asciidoctor.js 提供自己的模板转换器,这意味着如果您使用 Asciidoctor.js,您需要开发一套不同的模板。

关于在 Asciidoctor 中使用转换器模板,有三个关键点需要理解:

  • Asciidoctor 如何选择要使用的模板引擎。

  • Asciidoctor 将什么后端数据模型传递给模板。

  • 可用的模板名称。

让我们通过 Asciidoctor 的角度来研究模板,然后探讨常见的 API、辅助函数和调试。

模板引擎选择

Asciidoctor 使用 Tilt 来加载和调用模板。Tilt 是多种 Ruby 模板引擎的通用接口,由必需的 tilt gem 提供。

您可以使用 Tilt 支持的任何模板语言来组合模板。如果您使用的模板引擎需要额外的库(即 gem),则必须先安装它们。例如,要使用 Haml 编写的模板,您必须安装 haml gem。

Tilt 会检查模板的文件扩展名,将其与已注册且可用的模板引擎匹配,然后将模板传递给引擎进行调用。如果文件扩展名未识别,或者模板引擎未安装,此过程将失败。

模板具有双文件扩展名(例如,.html.haml)。外部文件扩展名是模板的文件扩展名。换句话说,它标识了模板语言。内部文件扩展名是输出文件的文件扩展名。换句话说,它标识了输出格式。

假设您有一个名为 paragraph.html.haml 的模板。.haml 文件扩展名告诉 Tilt 将其委托给 Haml。文件名剩余的部分(例如,paragraph.html)将用作输出文件。然而,在 Asciidoctor 中,模板的结果不会输出到文件。相反,它会与转换器的其余输出合并。但是文件名扩展名仍应与 Asciidoctor 生成的输出文件的扩展名匹配。

您可以在 Tilt README 中找到 Tilt 支持的模板引擎列表以及任何必需的库。最常用的模板引擎是Slim、Haml 和 ERB。我们强烈建议使用 Slim。ERB 也是一个可靠的选择,因为它内置于 Ruby 语言中,因此没有任何依赖项。我们鼓励您阅读您选择的模板引擎的文档,以了解如何使用该特定模板语言。

可用的模板名称

当我们谈论模板名称时,我们指的是模板的基名减去双文件扩展名。例如,模板 paragraph.html.slim 的名称是 paragraph。此名称很重要,因为它与 Asciidoctor 中的可转换上下文(不包括前导冒号)是一对一映射关系。可转换上下文大致是解析后的 AsciiDoc 文档中的节点。

如果模板名称与可转换上下文的名称匹配,则当在该节点上调用 convert 方法时,Asciidoctor 将使用该模板为具有该上下文的任何节点生成输出。您可以根据需要为任意数量的上下文创建模板。如果 Asciidoctor 无法找到可转换上下文的模板,它将回退到使用转换器提供的处理程序。通过使用模板,您可以自定义转换器生成的部分或全部输出。

请记住,只有当转换器支持时才能使用模板。Asciidoctor 中的所有内置转换器都支持使用模板来自定义转换器。

外部模板的名称是 document 或 embedded,具体取决于文档是否以独立模式进行转换。Asciidoctor 不会自行遍历文档树并调用相应的模板。相反,模板需要通过调用块节点上的content方法来触发其他模板。因此,如果您替换了 document 或 embedded 模板,而没有调用content方法,那么其他模板就不会被调用。

让我们通过后端数据模型来了解模板中可以使用的逻辑。

后端数据模型

Asciidoctor 中模板的后端数据模型始终是正在转换的节点(具体来说是 AbstractNode)。(Asciidoctor 文档模型中的节点类似于 XML DOM 节点)。例如,在为段落编写模板时,后端数据模型是上下文为 :paragraphBlock 实例。

您可以使用 self 关键字访问节点本身。

- puts self

在 Slim 模板中,以 - 开头的行执行 Ruby 语句。以 = 开头的表达式(无论是行首还是标签名称之后)都会调用一个方法并将返回值插入模板。

在模板中,您可以使用该成员的名称访问节点的*所有*实例变量和方法,就像在方法调用中一样(例如,@idtitle)。(您可以将模板视为节点对象上的方法调用)。引用访问器方法时,成员名称等同于模板变量。

通常不建议从模板访问节点的实例变量(例如,@id),因为它会使您的模板与内部模型紧密耦合。最好坚持使用公共访问器和方法。

要访问节点的已转换内容,请使用模板变量 content

p =content

语法 =content 调用节点上的 content 访问器方法,并将结果插入模板。

您可以通过 document 属性从模板访问所有节点的文档。文档最常用于查找文档属性,如下所示:

p lang=(document.attr 'lang') =content

您还可以使用 document 对象通过 Document#find_by 查找文档中的其他节点。

让我们看一个完整的段落模板示例,它模仿了内置 HTML 转换器的输出。

paragraph.html.slim
div id=id class=['paragraph', role]
  - if title?
    .title =title
  p =content

假设 id 为 "hello",title 为 "Hello, World!",role 为 nil,content 为 "Your first template!",则此模板将生成以下 HTML:

<div id="hello" class="paragraph">
  <div class="title">Hello, World!</div>
  <p>Your first template!</p>
</div>

此模板使用 idroletitlecontent 属性,以及 title? 方法。您可能会注意到 Slim 会为您推断一些逻辑。如果未设置 role,它将从数组中删除该条目,用空格连接其余条目,并输出 class 属性。如果 id 属性为 nil 而不是 "hello",则模板将不输出 id 属性。

为了帮助您了解节点上可用的属性和方法,您可以使用以下表达式打印它们:

- pp (public_methods - Object.instance_methods).reject {|it| it.end_with? '=' }

所有属性都报告为方法,这就是为什么此语句使用 public_methods

您可以使用以下方法检查当前节点和文档上所有可用的属性:

- pp attributes
- pp document.attributes

要了解有关这些属性和方法的更多信息,以及它们返回的内容,请参阅API 文档。还可以参考调试以了解有关如何检查后端数据模型的更多信息。

常用 API

每个节点共享一组通用属性,例如 id、role、attributes、context、其父节点和 document 节点。块节点和行内节点具有与其用途相关的其他属性。例如,块节点有一个 content 属性用于访问其已转换的内容,列表节点有一个 items 属性用于访问列表项,行内节点有一个 text 属性用于访问已转换的文本。

每个模板都可以访问文档模型中从正在转换的节点可访问的任何 API。下表提供了您可能最常使用的 API 列表。

名称 示例(Slim) 描述

document

- if document.attr 'icons', 'font'

对当前文档(及其所有节点)的引用。

content

=content

转换此块节点(如果有)的子节点并返回结果。

items

- items.each do |item|

提供对列表节点中项的访问。请注意,列表模板必须处理其自己的项。

文本

=text

返回此行内节点的已转换文本。

target

=target

返回此行内节点(如果适用)的已转换目标(例如,锚点、图像)。

ID

div id=id

分配给块的 id,如果没有分配 id 则为 nil。

角色

div class=role

一个方便的方法,它返回块的角色属性,如果块没有角色则返回 nil。

role?

if role? 'lead'

一个方便的方法,用于检查块是否具有角色属性。

attr

div class=(attr 'toc-class', 'toc')

检索元素上指定属性的值,使用名称作为键。如果名称写成符号,它将在查找之前自动转换为字符串。第二个参数是在未设置属性时的回退值。

attr?

- if attr? 'icons'

检查指定属性是否存在于元素上,使用名称作为键。如果名称写成符号,它将在查找之前自动转换为字符串。如果提供了第二个参数(匹配项),它还将检查属性值是否与指定值匹配。

style

- if style == 'source'

检索块节点的样式(限定符)。如果块没有样式,则返回 nil。

title

=title

检索已应用正常替换(转义 XML、渲染链接等)的块的标题。

title?

- if title?

检查此块是否已分配标题。此方法没有副作用(例如,仅检查存在性,不应用替换)。

captioned_title

=captioned_title

检索已应用标题和正常替换(转义 XML、渲染链接等)的块的标题。

option?

video autoplay=(option? 'autoplay')

一个方便的方法,用于检查指定的选项属性(例如,autoplay-option)是否存在。

type

- if type == :xref

返回具有变体的行内节点的节点变体(例如,anchor、quoted 等)。

image_uri

img src=(image_uri attr 'target')

将路径转换为要在 HTML img 元素中使用的图像 URI(引用或嵌入数据)。应用安全限制,清理路径,并在文档上启用了 :data-uri: 属性时嵌入图像数据。处理图像引用时始终使用此方法。相对图像路径相对于文档目录解析,除非使用 :imagesdir 覆盖。

icon_uri

img src=(icon_uri attr 'target')

与 image_uri 相同, except it specifically works with icons。默认情况下,它会在子目录 images/icons 中查找,除非使用 :iconsdir 覆盖。

media_uri

audio src=(media_uri attr 'target')

类似于 image_uri, except it does not support embedding the data into the document。 intended for video and audio paths。

normalize_web_path

link href=(normalize_web_path attr 'stylesheet')

将路径连接到 relative_root 并规范化父目录和自引用。对父目录的访问可能受安全模式设置的限制。

辅助函数

如前所述,模板的后端数据模型主要由正在转换的节点的属性和方法组成。模板引擎提供的辅助函数也可作为模板中的顶级函数使用。有关详细信息,请参阅模板引擎的文档。

如果您发现自己将大量逻辑放在模板中,您可能希望将该逻辑提取到自定义辅助函数中。在使用 Haml 或 Slim 时,您可以在与模板相同的文件夹中找到的 helper.rb 文件中定义这些辅助函数。这些辅助函数可以简化出现在多个模板中的重复元素。

辅助函数文件必须定义 Ruby 模块 Haml::HelpersSlim::Helpers,具体取决于您的模板面向哪个模板引擎。在此模块中定义的每个方法都将成为模板中的顶级函数。该方法实际上已混入节点中,因此函数中的 self 引用就是节点本身。

模板引擎提供的辅助函数也可作为顶级函数使用。例如,Haml 提供了 html_tag 辅助函数来动态创建 HTML 元素。有关详细信息,请参阅模板引擎的文档。

假设我们正在为节创建模板,并且我们希望输出节标题及其编号,但前提是自动节编号已启用。我们可以为此目的创建一个辅助函数。

helpers.rb
module Slim::Helpers
  def section_title
    if caption
      captioned_title
    elsif numbered && level <= (document.attr :sectnumlevels, 3).to_i
      if level < 2 && document.doctype == 'book'
        case sectname
        when 'chapter'
          %(#{(signifier = document.attr 'chapter-signifier') ? signifier.to_s + ' ' : ''}#{sectnum} #{title})
        when 'part'
          %(#{(signifier = document.attr 'part-signifier') ? signifier.to_s + ' ' : ''}#{sectnum nil, ':'} #{title})
        else
          %(#{sectnum} #{title})
        end
      else
        %(#{sectnum} #{title})
      end
    else
      title
    end
  end
end

您现在可以在节模板中使用此辅助函数,如下所示:

section.html.slim
*{ tag: %(h#{level + 1}) } =section_title (1)
=content (2)
1 我们正在利用 Slim 中的特殊语法来动态创建 HTML 标题元素。
2 调用 content 方法以转换包含其他节点节点的子节点是必要的。

如果您更喜欢纯函数形式的辅助函数,您可以将节点作为第一个参数传入,并且仅使用该引用来访问后端数据模型的属性。

helpers.rb 使用纯函数
module Slim::Helpers
  def section_title node = self
    if node.caption
      node.captioned_title
    elsif node.numbered && node.level <= (node.document.attr :sectnumlevels, 3).to_i
      ...
    else
      node.title
    end
  end
end

您选择哪种风格来编写辅助函数取决于您。但是,如果您发现需要为不同场景重用函数,那么纯函数的投入可能会物有所值。

调试

有两种方法可以通过探索后端模型来调试模板:

  • 使用 putspp 将消息和返回值打印到 STDOUT。

  • 使用交互式调试器跳转到模板的上下文。

要将当前节点以字符串形式打印到 STDOUT,您可以在模板中使用以下语句:

- puts self

您可以使用 pp 打印有关当前节点的结构化信息:

- pp self

但是,由于节点具有循环引用,因此输出可能会非常冗长。您可能会发现打印更具体的信息更有用。

您可以使用以下语句查看当前节点和文档上可用的属性:

- pp attributes
- pp document.attributes

您可以使用以下表达式查看节点上可用的属性和方法:

- pp (public_methods - Object.instance_methods).reject {|it| it.end_with? '=' }

使用打印语句,您必须更新模板并每次重新运行 Asciidoctor 才能继续检查。更有效的方法是使用交互式调试器。

使用交互式调试器

Pry 是一个强大的 Ruby 调试器,具有语法高亮、标签补全以及文档和源代码浏览功能。您可以使用它来交互式地发现 Asciidoctor 模板可用的后端模型对象层次结构。

要使用 Pry,您首先需要安装它,可以使用 gem install

$ gem install pry

或者将其添加到您的 Gemfile 中并运行 bundle

为了在模板的特定点被置于调试器中,请将以下两行添加到您要检查的模板中:

paragraph.html.slim
- require 'pry'
- binding.pry

当您运行 Asciidoctor 时,它将在模板中暂停,并为您提供一个交互式控制台。

From: /path/to/templates/html5/paragraph.html.slim:7 self.__tilt_800:

    1: - require 'pry'
 => 2: - binding.pry

[1] pry(#<Asciidoctor::Block>)>

从那里,您可以检查后端模型中的对象。

[1] pry(#<Asciidoctor::Block>)> attributes

您还可以查询 Asciidoctor 的 API 文档:

[1] pry(#<Asciidoctor::Block>)> ? find_by

键入 exit 以退出交互式控制台。

[1] pry(#<Asciidoctor::Block>)> exit

要了解有关 Pry 功能的更多信息,我们建议观看入门屏幕录像。有关如何使用 Pry 的详细信息,请参阅Pry wiki

如何使用模板

现在您知道什么是模板以及如何创建它们,让我们看看如何在 Asciidoctor 中使用它们。

组织您的模板

您应该将特定后端的模板分组到单个文件夹中。在该文件夹中,每个模板文件都应使用模式 <context><output-ext><template-ext> 命名,其中 context可转换上下文的名称,output-ext 是输出文件的文件扩展名,template-ext 是模板语言的文件扩展名(例如,paragraph.html.slim)。这些是 Asciidoctor 发现和加载模板的唯一要求。

如果您为多个后端创建模板,您可能会进一步将模板分组到以后端命名的文件夹中(甚至可能还有一个用于模板语言的附加文件夹,此处未显示)。

📒 templates (1)
  📂 html5 (2)
    📄 paragraph.html.slim (3)
1 包含各种后端模板的文件夹。
2 包含 html5 后端模板的文件夹。
3 段落的转换器模板。

如果您只针对单个后端,您可以简单地将文件夹命名为 templates

📒 templates
  📄 paragraph.html.slim

请记住,后端是预期输出格式的别名,反过来也是生成它的转换器。

安装模板引擎

要使用转换器模板,您必须始终安装 tilt gem。如果您使用的模板引擎有一个或多个必需的库,则必须先安装这些库。安装库后,Asciidoctor 将按需使用 Tilt 加载它。

如果您用 ERB 编写模板,则无需额外库。

假设您用 Slim 编写模板(这是我们最推荐的模板引擎)。您将需要同时安装 tiltslim gem。

如果您使用 Bundler,您首先通过在 Gemfile 中声明它们来安装 gem。

Gemfile
gem 'tilt'
gem 'slim'

然后,您使用 Bundler 安装 gem:

$ bundle

如果您不使用 Bundler,并且已将 Ruby 配置为将 gem 安装到您的用户/主目录中,则可以使用 gem 命令代替:

$ gem install tilt slim

无论哪种方式,在使用 Slim 模板语言编写的模板时,tiltslim gem 都必须在运行 Asciidoctor 时出现在加载路径上。

应用您的模板

指示 Asciidoctor 应用您的模板是最简单的部分。您只需要告诉 Asciidoctor 模板的位置以及您正在使用的模板引擎。(技术上,您不需要指定模板引擎。但是,通过这样做,可以使扫描更有效和确定)。

如果您使用 CLI,您可以使用 -T 选项(长格式:--template-dir)指定模板目录,并使用 -E 选项(长格式:--template-engine)指定模板引擎。

$ asciidoctor -T /path/to/templates -E slim doc.adoc

如果您使用 API,您可以使用 :template_dirs 选项指定模板目录(或目录),并使用 :template_engine 选项指定模板引擎。

Asciidoctor.convert_file 'doc.adoc', safe: :safe,
  template_dirs: ['/path/to/templates'], template_engine: 'slim'

请注意,我们在模板所在路径中没有指定 html5 段。这是因为 Asciidoctor 在扫描模板时会自动查找与后端名称匹配的文件夹(例如,/path/to/templates/html5)。但是,您可以选择包含后端段的路径。

使用多个模板目录

您可以将单个后端的模板分布在多个目录中。例如,您可能有一组适用于所有项目的通用模板(例如,/path/to/common-templates),以及一组补充和/或覆盖特定项目模板的专用模板(例如,/path/to/specialized-templates)。

要在使用 CLI 时从多个目录加载模板,您可以多次指定 -T 选项来将每个目录传递给 Asciidoctor:

$ asciidoctor -T /path/to/common-templates -T /path/to/specialized-templates -E slim doc.adoc

在使用 API 时,您将所有模板目录添加到 :template_dirs 选项的数组值中:

Asciidoctor.convert_file 'doc.adoc', safe: :safe,
  template_dirs: ['/path/to/common-templates', '/path/to/specialized-templates'], template_engine: 'slim'

在这两种情况下,如果同一模板在多个位置找到,则使用列表中后面列出的目录中找到的模板。

教程:您的第一个转换器模板

本节提供了一个教程,您可以按照该教程快速学习如何编写和使用您的第一个转换器模板。在本教程中,您将创建一个转换器模板来定制内置 HTML 转换器为无序列表生成的 HTML。您将使用 Slim 模板语言编写模板。然后,您将在使用 Asciidoctor 将 AsciiDoc 文档转换为 HTML 时使用该模板来观察此自定义结果。

添加和安装所需的 gem

您首先需要安装模板引擎所需的库(即 gem)。由于您将使用 Slim,因此需要安装 slim gem。您还需要安装 tilt gem,它提供了 Tilt,即 Asciidoctor 用于加载和调用模板的 Ruby 模板引擎的通用接口。虽然不是必需的,Asciidoctor 会提示您也安装 concurrent-ruby gem 以正确实现模板缓存。

安装 gem 的首选方法是将其添加到项目中的 Gemfile

Gemfile
source 'https://rubygems.org.cn'

gem 'asciidoctor'
# ...any other gems you are using
gem 'tilt'
gem 'slim'
gem 'concurrent-ruby'

运行 Bundler 将 gem 安装到您的项目中。

$ bundle

如果您不使用 Bundler,并且已将 Ruby 配置为将 gem 安装到您的用户/主目录中,则可以使用 gem 命令代替:

$ gem install tilt slim concurrent-ruby

现在您已经安装了 Tilt 和 Slim 模板引擎,您可以开始编写模板了。

创建 templates 文件夹

接下来,创建一个名为 templates 的新文件夹来存储您的模板。我们还建议创建一个名为 html5 的嵌套文件夹,以按后端组织模板。

$ mkdir -p templates/html5

您可以进一步按引擎将模板组织到文件夹中,但这并非必需。

编写模板

让我们编写模板来定制无序列表的 HTML。由于无序列表的上下文是 :ulist(请参阅可转换上下文),因此您将把模板命名为 ulist.html.slim

ulist.html.slim
- if title?
  figure.list.unordered id=id
    figcaption=title
    ul class=[style, role]
      - items.each do |_item|
        li
          span.primary=_item.text
          - if _item.blocks?
            =_item.content
- else
  ul id=id class=[style, role]
    - items.each do |_item|
      li
        span.primary=_item.text
        - if _item.blocks?
          =_item.content

应用模板

最后一步是在调用 Asciidoctor 时使用模板。创建一个名为 doc.adoc 的 AsciiDoc 文件,其中包含一个无序列表。

doc.adoc
* cats
* dogs
* birds

现在,您可以通过传递包含模板的目录(使用 -T 选项)和模板引擎的名称(使用 -E 选项)来指示 Asciidoctor 使用您的模板转换此列表。如果您使用 Bundler 安装了 gem,请按以下方式运行 Asciidoctor:

$ bundle exec asciidoctor -T templates -E slim doc.adoc

否则,您可以省略 bundle exec 前缀:

$ asciidoctor -T templates -E slim doc.adoc

现在您已经创建了第一个转换器模板,您已经走上了根据您的需求定制 Asciidoctor 生成的 HTML 的道路!

快速回顾

这是使用 Slim 编写的模板来自定义内置 HTML 5 转换器输出的快速回顾。

  1. 使用 bundlegem install 安装 tiltslimconcurrent-ruby gem。

  2. 创建一个名为 templates/html5 的文件夹来存储模板。

  3. 在该文件夹中创建一个名为 paragraph.html.slim 的模板。

  4. 用您自己的模板逻辑填充模板。这是一个简单的例子:

    paragraph.html.slim
    p id=id role=role =content
  5. 使用 CLI 的 -T 标志加载模板。

    $ bundle exec asciidoctor -T path/to/templates -E slim doc.adoc

    $ asciidoctor -T path/to/templates -E slim doc.adoc
通过 API 调用 Asciidoctor 时,通过将路径传递给 :templates 选项来加载模板。

我们希望您同意使用模板可以轻松定制 Asciidoctor 生成的输出。