模板转换器

在本页面中,您将学习如何使用模板来完全控制输出。

内置模板引擎

默认情况下,Asciidoctor.js 支持以下模板引擎及其对应的文件扩展名:

  • EJS

    • .ejs

  • Handlebars

    • .handlebars, .hbs

  • Nunjucks

    • .nunjucks, .njk

  • Pug

    • .pug

请注意,这些依赖项是可选的,因此您需要明确安装它们。例如,如果您想使用 Nunjucks,您需要安装 nunjucks 包:

npm i nunjucks

安装完依赖项后,您可以在目录中创建模板文件。

普通 JavaScript 模板

Asciidoctor.js 还支持用纯 JavaScript 编写的模板。在这种情况下,您应该编写一个导出默认函数的 JavaScript 文件:

module.exports = ({ node }) => `<p class="paragraph">${node.getContent()}</p>`

该函数将作为 模板上下文 的参数被调用。

命名约定

假设我们希望使用 Nunjucks 编写模板,我们创建一个名为 templates 的目录,并创建一个名为 paragraph.njk 的文件:

<p class="paragraph">{{ node.getContent() | safe }}</p>

默认情况下,Nunjucks 会自动对所有输出进行转义以保证安全。在此,我们使用内置的 safe 过滤器将输出标记为安全,这样 Nunjucks 就不会对该输出进行转义。

如上所述,文件扩展名 .njk 很重要,它告诉 Asciidoctor.js 这是一个 Nunjucks 模板。而且,文件名 paragraph 也很重要,因为它与节点名匹配。以下是节点名的完整列表:

  • document

  • embedded

  • outline

  • section

  • admonition

  • audio

  • colist

  • dlist

  • example

  • floating-title

  • image

  • listing

  • literal

  • stem

  • olist

  • open

  • page_break

  • paragraph

  • preamble

  • quote

  • thematic_break

  • sidebar

  • table

  • toc

  • ulist

  • verse

  • video

  • inline_anchor

  • inline_break

  • inline_button

  • inline_callout

  • inline_footnote

  • inline_image

  • inline_indexterm

  • inline_kbd

  • inline_menu

  • inline_quoted

您不需要为所有节点创建模板。Asciidoctor.js 可以使用内置的转换器来回退。例如,我们可以使用内置的 HTML5 转换器处理所有节点,只有 paragraph 节点使用自定义模板。

模板目录

您可以通过 CLI 使用 --template-dir 选项(或者简写为 -T)来指示 Asciidoctor.js 使用模板目录:

$ asciidoctor --template-dir ./templates doc.adoc

您还可以使用 API 配置模板目录:

asciidoctor.convertFile('doc.adoc', { safe: 'safe', backend: 'html5', template_dir: './templates' })

多个模板目录

您也可以使用多个模板目录。在这种情况下,您可以重复使用 CLI 中的 --template-dir 选项:

$ asciidoctor --template-dir ./templates-a --template-dir ./templates-b doc.adoc

在上述命令中,我们使用了两个模板目录,分别为 templates-atemplates-b

在 API 中,我们需要定义 template_dirs 选项:

asciidoctor.convertFile('doc.adoc', { safe: 'safe', backend: 'html5', template_dirs: ['./templates-a', './templates-b'] })

冲突解决

Asciidoctor.js 在以下情况中如何解决冲突:

同一目录中定义了两个或更多相同节点名的模板

例如,您在模板目录中有 paragraph.njkparagraph.hbs 文件。在这种情况下,规则是 “字母顺序中最后一个获胜”。由于 .njk 在字母顺序中排在 .hbs 后面,Asciidoctor.js 会使用 Nunjucks 模板而不是 Handlebars 模板。

不同目录中定义了相同节点的模板

例如,template-a 目录中有 paragraph.njk,而 template-b 目录中也有 paragraph.njk。在这种情况下,规则仍然是 “字母顺序中最后一个获胜”,但模板目录中的顺序很重要。如果我声明如下:

const options = { template_dirs: ['template-a', 'template-b'] }

那么 template-b/paragraph.njk 会获胜,因为它实际上是最后一个。如果我改变 template_dirs 选项中的顺序:

const options = { template_dirs: ['template-b', 'template-a'] }

那么 template-a/paragraph.njk 会获胜!

请注意,这并不是推荐的做法,您应该尽量避免上游冲突。

helpers.js 文件

您可以在模板目录中创建一个 helpers.js 文件,用于声明可以在模板中使用的工具函数。例如,如果您使用 Handlebars,您可能希望注册部分或帮助函数;类似地,如果您使用 Nunjucks,您可能希望添加自定义过滤器。

如果此文件存在,Asciidoctor.js 将加载它(使用 Node.js require 指令),并在导出时调用 configure 函数:

helpers.js
module.exports.configure = (context) => {
  // ...
}

上下文对象将包含一个隔离的环境(如果支持),其中包含按模板引擎名称键入的模板引擎:

handlebars.environment

通过 Handlebars.create() 获取的隔离 Handlebars 环境

nunjucks.environment

通过 nunjucks.configure() 获取的隔离 Nunjucks 环境

以下是一个具体示例,我们添加了一个名为 shorten 的 Nunjucks 过滤器,它返回字符串的前 count 个字符,count 默认为 5:

helpers.js
module.exports.configure = (context) => {
  context.nunjucks.environment.addFilter('shorten', (str, count) => str.slice(0, count || 5))
}

隔离环境

隔离环境意味着每个环境都有自己的帮助函数、部分、过滤器等。需要注意的是,每个模板目录都有自己的隔离环境。例如,如果我们在两个目录中定义了相同名称的值,最后一个定义的值不会覆盖第一个:

web/helpers.js
module.exports.configure = (context) => {
  context.nunjucks.environment.addGlobal('cdn', '//cdn.web.com')
}
blog/helpers.js
module.exports.configure = (context) => {
  context.nunjucks.environment.addGlobal('cdn', '//cdn.blog.io')
}

在上述定义中,cdn 的值将会是:

  • //cdn.web.com,如果我们使用模板目录 web

  • //cdn.blog.io,如果我们使用模板目录 blog

无状态

EJS、普通 JavaScript 和 Pug 模板不依赖于 “环境”。因此,您不需要定义 configure 函数。相反,您可以在 helpers.js 文件中导出值和函数,这些函数和变量将可以在所有模板中访问:

helpers.js
let assetUriScheme
module.exports.version = '1.0.0'
module.exports.getAssetUriScheme = (document) => {
  if (assetUriScheme) {
    return assetUriScheme
  }
  const scheme = document.getAttribute('asset-uri-scheme', 'https')
  assetUriScheme = (scheme && scheme.trim() !== '') ? `${scheme}:` : ''
  return assetUriScheme
}

在上面的示例中,versiongetAssetUriScheme 函数将会在模板上下文中的 helpers 键下使用:

video.js
module.exports = function ({ node, _, helpers }) {
  const target = node.getAttribute('target')
  const document = node.getDocument()
  const src = `${helpers.getAssetUriScheme(document)}//www.youtube.com/embed/${target}}` (1)
  return `<figure class="video"><iframe src="${src}" frameborder="0"/></figure>`
}
1 使用在 helpers.js 文件中定义的 getAssetUriScheme 函数

模板上下文

Asciidoctor.js 会将以下上下文传递给模板:

node

Asciidoctor.js AST 中的 AbstractNode。根据上下文,它可以是 Section、Document、Block 等。 我们建议阅读 JS API 文档 以了解每个节点中可用的内容。

opts

可选的选项 JSON。

helpers

helpers.js 文件中导出的函数和变量。

模板选项

您可以使用 template_engine_options 选项来配置模板引擎。以下是一些示例:

const options = {
  template_engine_options: {
    nunjucks: {
      autoescape: false
    },
    handlebars: {
      noEscape: true
    },
    pug: {
      doctype: 'xml'
    },
    ejs: {
      delimiter: '?',
      openDelimiter: '[',
      closeDelimiter: ']'
    }
  }
}

要了解可以使用哪些选项,请阅读您所使用的模板引擎的官方文档:

模板缓存

出于性能考虑,模板会被缓存,但您可以使用 template_cache 选项禁用此功能:

asciidoctor.convert(input, { template_cache: false })

当您希望使用不同的选项配置相同的模板目录时,这可能会很有用。在以下示例中,我们希望设置 XML 文档类型。如果不禁用缓存,第二次转换将不会重新配置带有 doctype: 'xml' 选项的模板:

const options = {
  safe: 'safe',
  doctype: 'inline',
  backend: 'html5',
  template_dir: '/path/to/templates/pug',
  template_cache: false, // 禁用模板缓存
}

console.log(asciidoctor.convert(`image:cat.png[]`, options)) // <img src="cat.png"/>
console.log(asciidoctor.convert('image:cat.png[]', Object.assign(options, {
  template_engine_options: {
    pug: {
      doctype: 'xml'
    }
  }
}))) // <img src="cat.png"></img>