Asciidoctor 生成的所有输出都是可自定义的。自定义输出的一种方式是使用自定义转换器。转换器模板提供了一个更简单的方法。
内置转换器,以及其他启用了 supports_templates
特性的转换器,允许您通过模板替换任何可转换上下文的转换处理程序。这一机制的目标是使定制转换器(例如,HTML)的输出更加容易,而无需开发、注册和使用自定义转换器。换句话说,您可以根据菜单定制转换器的输出,而无需编写代码(模板中的除外)。
这个页面解释了转换器模板机制在Asciidoctor中是如何工作的,以及您如何利用它来自定义支持该机制的转换器的输出。
什么是模板?
模板是一种专注于产生输出的源文件。当我们在这种情境中使用术语[.term]_模板_时,我们特指用于Ruby模板引擎的源文件。
你可以将模板视为源代码,其中输出文本形成主要结构,编程逻辑交织其间。如果你有编程背景,就好像程序逻辑和字符串颠倒了一样。
当你编写模板时,你从静态文本开始。然后你在文本的各个区域周围散布逻辑。这些逻辑让你可以基于一个支持的数据模型提供的信息,有条件地选择文本或者重复文本。那些散布的逻辑的语法就是模板语言。
Ruby中最常见的模板引擎是ERB,因为它内置在语言中。这里有一个ERB模板的例子,它使用由后端数据模型提供的内容输出一个段落元素:
<p><%= content %></p>
一些模板语言,比如Slim和Haml,结构更加严谨。下面是先前例子用Slim写的版本:
p = 内容
在Haml中:
%p = 内容
注意这些模板中没有角括号。这是因为Slim和Haml假设每条语句的开始都是XML元素的名称(Haml要求以`%为首)。等号告诉Slim和Haml去评估其后的Ruby表达式并将结果插入到输出中。在这个例子中,模板正在读取变量`content
,技术上是后端数据模型的一个属性。Slim和Haml使用缩进来推断HTML结构中的嵌套(很像在YAML中一样)。
在运行时,模板引擎将模板转换为可运行的代码并调用它。我们将这个操作称为调用模板。实际上,这个操作调用了模板中的逻辑并展开了模板中的变量引用,以产生解析后的输出。
既然你对模板的基本概念已经熟悉了,那么让我们来看看在Asciidoctor中是如何使用模板的。
Asciidoctor中的模板
在Asciidoctor中,模板被用来自定义由转换器生成的输出(只要转换器启用了`supports_templates`特性)。我们将这些模板称为转换器模板。转换器模板与它们所应用的转换器共同工作。
你可以在Asciidoctor和AsciidoctorJ之间重用你开发的同一套模板,这使得模板在两个运行时之间可以移植。Asciidoctor.js提供了它自己的template converter,这意味着如果你在使用Asciidoctor.js的话,你必须开发一套不同的模板。
在Asciidoctor中使用转换器模板需要理解三个关键点。
-
Asciidoctor如何选择使用的模板引擎。
-
Asciidoctor将哪种后台数据模型传递给模板。
-
可用的模板名称。
让我们通过Asciidoctor的视角来研究模板,然后探索常见的API、助手和调试技巧。
模板引擎选择
Asciidoctor 使用 Tilt 来加载和调用模板。Tilt 是对多个 Ruby 模板引擎的通用接口,由必需的 tilt 宝石提供。
您可以使用Tilt支持的任何模板语言来编写模板。如果您使用的模板引擎需要额外的库(例如,gems),您必须先安装它们。例如,要使用Haml编写的模板,您必须安装 haml 宝石。
Tilt检查模板的文件扩展名,将其与注册且可用的模板引擎进行匹配,并将模板传递给引擎以供调用。如果文件扩展名未被识别,或模板引擎未安装,则此过程将失败。
一个模板有一个双重文件扩展名(例如,.html.haml
)。外部文件扩展名是模板的文件扩展名。换句话说,它标识了模板语言。内部文件扩展名是输出文件的文件扩展名。换句话说,它标识了输出格式。
让我们假设你有一个名为[.path]paragraph.html.haml的模板。.haml_文件扩展名告诉Tilt委托给Haml处理。文件名的其余部分(例如,paragraph.html)将被用作输出文件的名称。然而,在Asciidoctor中,模板的结果不会输出到文件中。相反,它与转换器的其余输出结合在一起。但文件扩展名仍应与Asciidoctor生成的输出文件的文件扩展名匹配。
你可以在Tilt的README文档中找到Tilt支持的模板引擎列表以及任何所需的库。最受欢迎的模板引擎包括Slim、Haml和ERB。我们强烈推荐使用Slim。ERB也是一个不错的选择,因为它内置于Ruby语言中,因此没有任何依赖。我们鼓励你阅读你选择的模板引擎的文档,以了解如何使用特定的模板语言。
可用的模板名称
当我们讨论模板的名称时,我们指的是模板的基本名称减去双文件扩展名。例如,模板[path]_paragraph.html.slim的名称是paragraph。这个名称很重要,因为它与Asciidoctor中的可转换上下文一一对应(不包括前导冒号)。可转换的上下文大致对应于解析后AsciiDoc文档中的节点。
如果一个模板的名字与一个可转换上下文的名字匹配,当在该节点上调用转换方法时,Asciidoctor将使用该模板来生成任何具有该上下文节点的输出。您可以为多个或尽可能少的上下文创建模板。如果Asciidoctor无法找到一个可转换上下文的模板,它将回退到使用转换器提供的处理程序。通过使用模板,你可以自定义转换器产生的部分或全部输出。
Note
|
请记住,只有当转换器支持时,才能使用模板。Asciidoctor中所有内置的转换器都支持使用模板来自定义转换器。 |
外部模板要么名为文档(document),要么名为嵌入式(embedded),这取决于文档是否在独立模式下转换。Asciidoctor不会自己遍历文档树并调用相应模板。相反,需要模板通过在块节点上调用`content`方法来触发其他模板。因此,如果你替换了文档(document)或嵌入式(embedded)模板,并且没有调用`content`方法,这就解释了为什么没有其他模板被调用。
让我们看看后端数据模型,以了解可以在模板中使用的逻辑。
支持数据模型
在Asciidoctor中,模板的后台数据模型始终是正在转换的节点(特别是一个[AbstractNode
]抽象节点)。(Asciidoctor文档模型中的节点类似于XML DOM节点)。例如,当为段落编写模板时,后台数据模型是一个具有`:paragraph`上下文的[Block
]块实例。
你可以使用 self
关键字来访问节点本身。
- puts self
在Slim模板中,以`-开头的行将执行一个Ruby语句。以
=`开头的表达式(不论是在行首或者在标签名称之后)调用一个方法并将返回值插入到模板中。
在模板中,您可以使用成员的名称访问节点的所有实例变量和方法,就像您在方法调用中做的那样(例如,@id
或 title
)。(您可以将模板视为对节点对象的方法调用)。引用访问器方法时,成员的名称与模板变量同义。
Caution
|
从模板中访问节点的实例变量(例如,@id )通常不被鼓励,因为它将你的模板与内部模型紧密耦合。最好坚持使用公共存取器和方法。
|
要访问节点转换后的内容,您使用模板变量`content`。
p = 内容
语法 =content
会调用节点上的 content
访问器方法,并将结果插入到模板中。
你可以通过模板中的`document`属性访问所有节点的文档。文档最常用于查找文档属性,如下所示:
p lang=(document.attr 'lang') =content
您也可以使用文档对象,通过`Document#find_by`来查找文档中的其他节点。
让我们看一个完整的段落模板示例,它模仿内置HTML转换器的输出。
div id=id class=['paragraph', role] - if title? .title =title p =content
假设id为"hello",title为"Hello, World!",role为nil,content为"Your first template!",这个模板将会产生以下HTML:
html <div id="hello" class="paragraph"> <div class="title">Hello, World!</div> <p>Your first template!</p> </div>
这个模板使用了 id
、role
、title
和 content
属性,以及 title?
方法。您可能会注意到Slim为您推断了一些逻辑。如果没有设置角色,它将从数组中删除该条目,将剩余的条目以空格连接,并输出class属性。如果 id
属性是nil而不是"hello",模板将不会输出id属性。
为了帮助你理解节点上有哪些属性和方法,你可以使用以下表达式来打印它们:
- pp (public_methods - Object.instance_methods).reject {|it| it.end_with? '=' }
所有属性都被报告为方法,这就是为什么这个语句使用`public_methods`的原因。
您可以按照以下方式检查当前节点和当前文档上可用的所有属性:
- pp attributes - pp document.attributes
为了更深入地了解这些属性和方法以及它们的返回值,请参阅{url-api-gems}/asciidoctor/{release-version}/Asciidoctor/AbstractNode[API文档]。同时,请参阅[Debugging]章节,了解更多关于如何检查底层数据模型的信息。
常用APIs
每个节点共有一组共同的属性,比如 id、role、attributes、context、其父节点以及文档节点。块级和内联节点有额外的特定于其目的的属性。例如,一个块节点有一个`content`属性来访问它的转换后的内容,一个列表节点有一个`items`属性来访问列表项,而一个内联节点有一个`text`属性来访问转换后的文本。
每个模板都可以访问文档模型中从被转换的节点可访问的任何API。下表提供了你可能最常使用的API列表。
Name | Example (Slim) | Description |
---|---|---|
文档 |
|
当前文档(及其所有节点)的引用。 |
内容 |
|
转换此块节点的子节点(如果有的话),并返回结果。 |
项目 |
|
提供对列表节点中项目的访问。 请注意,列表模板必须处理自己的项目。 |
文本 |
|
返回此内联节点的转换后文本。 |
目标 |
|
返回此内联节点的转换后目标(如适用),例如锚点,图像。 |
id |
|
分配给块的id,如果没有分配id则为nil。 |
角色 |
|
返回块的角色属性的便利方法,如果块没有角色,则为nil。 |
角色? |
|
检查块是否具有角色属性的便利方法。 |
属性 |
|
使用名称作为键检索元素上指定属性的值。 如果名称写为符号,则在查找前会自动转换为字符串。 如果未设置属性,则第二个参数是一个后备值。 |
attr? |
|
使用名称作为键检查元素上是否存在指定属性。 如果名称写为符号,则在查找前会自动转换为字符串。 如果提供了第二个参数(一个匹配项),它还会检查属性值是否与指定值匹配。 |
样式 |
|
检索块节点的样式(限定符)。 如果块没有样式,则返回nil。 |
标题 |
|
检索应用了正常替换(转义XML,呈现链接等)的块标题。 |
标题? |
|
检查此块是否已分配标题。 此方法没有副作用(例如,仅检查存在性,不应用替换) |
带标题的标题 |
|
检索应用了标题和正常替换(转义XML,呈现链接等)的块标题。 |
选项? |
|
检查是否存在指定的选项属性(例如,自动播放选项)的便捷方法。 |
类型 |
|
返回具有变体的内联节点的节点变体(例如,锚点,引用等)。 |
图像URI |
|
将路径转换为用于HTML img元素的图像URI(引用或嵌入数据)。 应用安全限制,清理路径,并且如果在文档上启用了:data-uri:属性,则可以嵌入图像数据。 处理图像引用时始终使用此方法。 如果没有使用:imagesdir:重写,则相对图像路径会相对于文档目录解析。 |
图标URI |
|
与image_uri相同,但专门用于处理图标。 默认情况下,它会在子目录images/icons中查找,除非使用:iconsdir:进行重写。 |
媒体URI |
|
类似于image_uri,但不支持将数据嵌入到文档中。 用于视频和音频路径。 |
规范化Web路径 |
|
将路径与relative_root(相对根路径)连接,并规范化父级和自身引用。根据安全模式设置,可能会限制访问父目录。 |
助手
如前所述,模板的基础数据模型主要由要转换的节点的属性和方法组成。模板引擎提供的辅助功能也作为模板中的顶级函数可用。有关详细信息,请参阅模板引擎的文档。
如果你发现自己在模板中放了很多逻辑,你可能会想要将这些逻辑抽取到自定义的助手函数中。当使用Haml或Slim时,你可以在与模板同一文件夹中定位的[.path]_helper.rb_文件中定义这些助手函数。这些助手函数可以简化在多个模板中出现的重复元素。
助手文件必须定义Ruby模块`Haml::Helpers`或`Slim::Helpers`,具体取决于你的模板目标是哪种模板引擎。在该模块中定义的每个方法都会成为模板中的顶级函数。该方法实际上被混入节点中,因此函数中的`self`引用就是节点本身。
Tip
|
模板引擎提供的辅助功能也可以用作顶级函数。例如,Haml提供了`html_tag`辅助功能,用于动态创建一个HTML元素。具体详情请参考模板引擎的文档。 |
让我们假设我们正在为章节创建一个模板,并且我们想要输出带有章节编号的章节标题,但仅在启用自动章节编号时才这样做。我们可以为此目的创建一个辅助函数:
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
现在您可以按如下方式在您的部分模板中使用这个助手:
*{ tag: %(h#{level + 1}) } =section_title (1) =content (2)
-
我们正在利用Slim中的一种特殊语法动态地创建HTML标题元素。
-
当一个节点包含其他节点时,必须调用
content
方法来转换该节点的子节点。
如果你希望你的助手函数是纯函数,你可以将节点作为第一个参数传入,并且只使用该引用来访问数据模型背后的属性。
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
你决定用哪种风格来编写你的助手程序完全取决于你。但是,如果你发现你需要在不同的场景中重用一个函数,你可能会发现投资于纯函数是值得的。
调试
通过探索背后的模型来调试模板有两种方法:
-
使用`puts`或`pp`打印消息并返回值到标准输出(STDOUT)。
-
使用交互式调试器跳转到模板的上下文中
打印调试信息
要将当前节点以字符串形式打印到标准输出(STDOUT),你可以在你的模板中使用以下语句:
- puts self
你可以使用`pp`打印有关当前节点的结构化信息。
- pp self
然而,由于节点具有循环引用,输出可能会非常详细。你可能会发现打印更具体的信息更有用。
你可以使用这些语句来查看当前节点和文档上有哪些可用的属性:
- pp attributes - pp document.attributes
您可以通过以下表达式查看节点上有哪些属性和方法:
- pp (public_methods - Object.instance_methods).reject {|it| it.end_with? '=' }
使用 print 语句时,你需要更新模板并重新运行 Asciidoctor 以便进一步检查。一个更有效的方法是使用交互式调试器。
使用一个交互式调试器
'''Pry 是一个功能强大的 Ruby 调试器,具有语法高亮、标签补全、文档和源码浏览等功能。你可以使用它来交互式地发现 Asciidoctor 模板可用的后端模型的对象层次结构。'''
要使用Pry,你首先需要安装它,可以使用`gem install`命令进行安装:
$ gem install pry
或者将其添加到你的 [Gemfile] 文件中并运行 bundle
命令。
为了在模板的特定点进入调试器,请在您想要检查的模板中添加以下两行:
- 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做什么,我们推荐观看介绍性的屏幕广播[http://vimeo.com/26391171]。有关如何使用它的详细信息,请参考[Pry wiki](https://github.com/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)
-
包含各种后端模板的文件夹。
-
包含 html5 后端模板的文件夹。
-
段落转换器模板。
如果你只针对单一后端,可以简单地将文件夹命名为“templates”。
📒 templates 📄 paragraph.html.slim
请记住,后端是期望输出格式的代名词,反过来,也是生成该格式的转换器。
安装模板引擎
要使用转换器模板,您必须始终安装 tilt 宝石(gem)。如果您使用的模板引擎有一个或多个必需的库,则必须先安装这些库。一旦安装了库,Asciidoctor 将使用 Tilt 按需加载它。
Tip
|
如果你用ERB写你的模板,不需要额外的库。 |
假设您使用的是Slim模板引擎(这是我们最推荐的模板引擎)。您需要安装*tilt*和*slim*两个宝石(gems)。
如果你正在使用Bundler,你首先通过在Gemfile中声明它们来安装宝石。
gem 'tilt' gem 'slim'
然后,你使用Bundler安装宝石(gems)。
$ bundle
如果你没有使用Bundler,并且你已经配置Ruby在你的用户/家目录下安装gems,那么你可以使用`gem`命令来代替:
$ gem install tilt slim
无论如何,在运行Asciidoctor以使用用Slim模板语言编写的模板时,*tilt*和*slim*宝石(gems)必须在加载路径上可用。
应用你的模板
指导Asciidoctor应用你的模板是最简单的部分。你只需要告诉Asciidoctor模板位于哪里以及你使用的是哪种模板引擎。(技术上,你不需要指定模板引擎。然而,这样做会使扫描更高效和确定性。)
如果您使用的是CLI,您可以使用`-T`选项(完整写法:--template-dir
)来指定模板目录,并使用`-E`选项(完整写法:--template-engine
)来指定模板引擎。
$ asciidoctor -T /path/to/templates -E slim doc.adoc
在使用JRuby时,你可以通过在路径前加上`uri:classloader:`来引用类路径上的目录。例如:
$ asciidoctor -r /path/to/templates.jar -T uri:classloader:/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'
请注意,我们没有在模板所在的路径中指定段落[.path]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时使用该模板,你将观察到这种自定义的结果。
添加并安装所需的宝石(gems)
首先您需要为模板引擎安装所需的库(即宝石)。由于您将使用Slim,您需要安装*slim*宝石。您还需要安装*tilt*宝石,它提供了Tilt,这是Ruby模板引擎的通用接口,Asciidoctor使用它来加载和调用模板。
安装宝石的首选方法是将其添加到项目中的 Gemfile
文件中。
gem 'tilt' gem 'slim'
运行Bundler来将宝石安装到你的项目中。
$ bundle
如果你没有使用Bundler,并且你已经配置Ruby在你的用户/家目录下安装gems,那么你可以使用`gem`命令来代替:
$ gem install tilt slim
现在您已经安装了Tilt和Slim模板引擎,您可以开始编写模板了。
创建模板文件夹
接下来,创建一个名为_templates_的新文件夹来存储你的模板。我们还建议创建一个名为_html5_的嵌套文件夹,以便按后端组织模板。
$ mkdir -p templates/html5
编写一个模板
让我们编写模板来自定义无序列表的HTML。由于无序列表的上下文是 :ulist
(参见contexts-ref.html),所以你将把模板命名为[.path]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时使用模板。你可以通过使用`-T`选项传递包含模板的目录,并使用`-E`选项传递模板引擎的名称来做到这一点。
$ asciidoctor -T templates -E slim doc.adoc
现在您已经创建了第一个转换器模板,您正在好好地按照自己的需求定制Asciidoctor生成的HTML!
快速回顾
这是一个关于如何开始使用写在Slim中的模板来自定义内建的HTML 5转换器输出的快速回顾。
-
使用
bundle
或者gem install
安装tilt
和slim
宝石(即gems)。 -
创建一个名为[.path]_templates/html5_的文件夹以存储模板。
-
在那个文件夹里创建一个名为[.path]_paragraph.html.slim的模板。
-
使用你自己的模板逻辑填充模板。这里有一个简单的例子:
paragraph.html.slimp id=id role=role =content
-
从命令行界面使用-T标志加载模板:
$ asciidoctor -T path/to/templates -E slim doc.adoc
通过API调用Asciidoctor时,可以通过传递路径到`:templates`选项来加载模板。
我们希望你会同意,使用模板可以轻松地定制 Asciidoctor 产生的输出内容。