扩展使用案例
本页面提供了一系列简单示例目录,展示了如何通过使用扩展来增强Antora的功能。每个部分介绍了一个不同的用例,并呈现了您可以构建的扩展代码作为起点。
您还可以参考Antora项目提供的官方扩展项目,以研究更复杂的示例。
设置全局AsciiDoc属性
如果您想定义具有动态值的全局AsciiDoc属性,可以使用扩展来实现。Playbook保存AsciiDoc配置对象,该对象本身包含全局AsciiDoc属性。扩展可以监听playbookBuilt
事件,并向该映射添加属性。
module.exports.register = function () {
this.on('beforeProcess', ({ siteAsciiDocConfig }) => {
const buildDate = new Date().toISOString()
siteAsciiDocConfig.attributes['build-date'] = buildDate
})
}
扩展还可以从文件或环境变量中读取这些值。
如果您需要设置针对组件版本范围的AsciiDoc属性,则需要监听contentClassified
事件。从那里,您可以访问组件版本对象上的asciidoc
属性,以获取AsciiDoc属性。您可以使用内容目录对象上的getComponentVersion
方法按名称和版本查找组件版本。或者,您可以从内容目录对象上的getComponents
方法返回的每个组件的versions
属性中访问组件版本。
打印AsciiDoc属性
如果您正在排除站点问题,可以使用扩展生成站点级别和每个组件版本的AsciiDoc属性报告。在制作此报告时,您可以选择是否要显示AsciiDoc属性,如它们在页面上可用(已编译)或如定义的(未编译)。
您可以使用以下扩展打印为每个组件版本编译的所有AsciiDoc属性。该扩展还打印从playbook编译的所有属性,尽管请记住这些属性已集成到每个组件版本的属性中。
module.exports.register = function () {
this.once('contentClassified', ({ siteAsciiDocConfig, contentCatalog }) => {
console.log('站点范围属性(已编译)')
console.log(siteAsciiDocConfig.attributes)
contentCatalog.getComponents().forEach((component) => {
component.versions.forEach((componentVersion) => {
console.log(`${componentVersion.version}@${componentVersion.name} 属性(已编译)`)
if (componentVersion.asciidoc === siteAsciiDocConfig) {
console.log('与站点范围属性相同')
} else {
console.log(componentVersion.asciidoc.attributes)
}
})
})
})
}
您可以使用以下扩展打印为每个组件版本(按来源)在playbook和antora.yml文件中定义的所有AsciiDoc属性。
module.exports.register = function () {
this.once('contentClassified', ({ playbook, contentCatalog }) => {
console.log('站点范围属性(如playbook中定义)')
console.log(playbook.asciidoc.attributes)
contentCatalog.getComponents().forEach((component) => {
component.versions.forEach((componentVersion) => {
getUniqueOrigins(contentCatalog, componentVersion).forEach((origin) => {
console.log(`${componentVersion.version}@${componentVersion.name} 属性(如在antora.yml中定义)`)
console.log(origin.descriptor.asciidoc?.attributes || {})
})
})
})
})
}
function getUniqueOrigins (contentCatalog, componentVersion) {
return contentCatalog.findBy({ component: componentVersion.name, version: componentVersion.version })
.reduce((origins, file) => {
const origin = file.src.origin
if (origin && !origins.includes(origin)) origins.push(origin)
return origins
}, [])
}
在编写其他扩展时,您可能会发现使用这些AsciiDoc属性集合很有用。
排除私有内容源
如果某些贡献者或CI作业没有权限访问playbook中的私有内容源,您可以使用扩展来过滤它们,而无需修改playbook文件。
此扩展在playbookBuilt
事件期间运行。它检索playbook,遍历内容源,并删除任何被检测为私有且因此需要身份验证的内容源。我们将依赖一种约定来向扩展传达哪些内容源是私有的。该约定是使用以git@
开头的SSH URL。Antora会自动将SSH URL转换为HTTP URL,因此使用此语法仅仅作为用户和扩展的提示,表明该URL是私有的,并且将请求身份验证。
module.exports.register = function () {
this.on('playbookBuilt', function ({ playbook }) {
playbook.content.sources = playbook.content.sources
.filter(({ url }) => !url.startsWith('git@'))
this.updateVariables({ playbook })
})
}
此扩展之所以有效,是因为playbook在此事件结束前是可变的,Antora在此时将其冻结。调用this.updateVariables
来替换生成器上下文中的playbook
变量并不是必需的,但在这里用于表达意图并未来保护扩展。
取消发布标记页面
如果您不希望某个页面被发布,可以在文件名前加下划线(例如,_hidden.adoc)。但是,如果您只希望页面在特定条件下不被发布,则需要使用扩展。
使用此扩展,任何设置了page-unpublish
页面属性的页面将不会被发布(意味着它将被取消发布)。例如:
= 秘密页面
:page-unpublish:
此页面将不会被发布。
您可以基于另一个AsciiDoc属性的存在(或不存在)来设置page-unpublish
页面属性。例如:
= 秘密页面
ifndef::include-secret[:page-unpublish:]
此页面将不会被发布。
此扩展在documentsConverted
事件期间运行。这是提供对虚拟文件上AsciiDoc元数据访问的最早事件。该扩展遍历内容目录中所有可发布页面,并取消发布设置了page-unpublish
属性的任何页面。为了取消发布页面,扩展会删除虚拟文件上的out
属性。如果out
属性不存在,则页面将不会被发布。
module.exports.register = function () {
this.on('documentsConverted', ({ contentCatalog }) => {
contentCatalog.getPages((page) => page.out).forEach((page) => {
if (page.asciidoc?.attributes['page-unpublish'] != null) {
delete page.out
}
})
})
}
请注意,未发布页面可能会有对它的引用。虽然Antora会解析这些引用,但引用的目标将不可用,这将导致Web服务器返回404响应。
如果您希望更精细地控制页面何时取消发布,您可以编写一个替换convertDocument
或convertDocuments
函数的扩展。这样做可以让您在其他页面引用它之前取消发布页面,以便它们显示为警告。
报告未列出的页面
在创建新页面后,很容易忘记将其添加到导航中,以便读者可以访问。我们可以使用扩展来识别未包含在导航中的页面,并使用记录器报告它们。
此扩展在 navigationBuilt
事件期间运行。它遍历每个组件版本,检索其内部导航条目的扁平列表,然后检查是否有任何未包含在该列表中的页面,通过URL比较页面。如果找到任何这样的页面,它将创建一个报告,可选择将它们添加到导航中。
module.exports.register = function ({ config }) {
const { addToNavigation, unlistedPagesHeading = '未列出的页面' } = config
const logger = this.getLogger('unlisted-pages-extension')
this
.on('navigationBuilt', ({ contentCatalog }) => {
contentCatalog.getComponents().forEach(({ versions }) => {
versions.forEach(({ name: component, version, navigation: nav, url: defaultUrl }) => {
const navEntriesByUrl = getNavEntriesByUrl(nav)
const unlistedPages = contentCatalog
.findBy({ component, version, family: 'page' })
.filter((page) => page.out)
.reduce((collector, page) => {
if ((page.pub.url in navEntriesByUrl) || page.pub.url === defaultUrl) return collector
logger.warn({ file: page.src, source: page.src.origin }, '检测到未列出的页面')
return collector.concat(page)
}, [])
if (unlistedPages.length && addToNavigation) {
nav.push({
content: unlistedPagesHeading,
items: unlistedPages.map((page) => {
return { content: page.asciidoc.navtitle, url: page.pub.url, urlType: 'internal' }
}),
root: true,
})
}
})
})
})
}
function getNavEntriesByUrl (items = [], accum = {}) {
items.forEach((item) => {
if (item.urlType === 'internal') accum[item.url.split('#')[0]] = item
getNavEntriesByUrl(item.items, accum)
})
return accum
}
您可以在 扩展教程 中阅读更多关于此扩展及如何配置它的信息。
取消发布未列出的页面
与报告未列出的页面不同,您可以选择从发布中移除这些页面。这是您可以使用导航来驱动哪些页面被发布的一种方式。
此扩展在 navigationBuilt
事件期间运行。它遍历每个组件版本,检索其内部导航条目的扁平列表,然后检查是否有任何未包含在该列表中的页面,通过URL比较页面。如果找到任何这样的页面,它将取消发布它们。
module.exports.register = function ({ config }) {
this
.on('navigationBuilt', ({ contentCatalog }) => {
contentCatalog.getComponents().forEach(({ versions }) => {
versions.forEach(({ name: component, version, navigation: nav, url: defaultUrl }) => {
const navEntriesByUrl = getNavEntriesByUrl(nav)
const unlistedPages = contentCatalog
.findBy({ component, version, family: 'page' })
.filter((page) => page.out)
.reduce((collector, page) => {
if ((page.pub.url in navEntriesByUrl) || page.pub.url === defaultUrl) return collector
return collector.concat(page)
}, [])
if (unlistedPages.length) unlistedPages.forEach((page) => delete page.out)
})
})
})
}
function getNavEntriesByUrl (items = [], accum = {}) {
items.forEach((item) => {
if (item.urlType === 'internal') accum[item.url.split('#')[0]] = item
getNavEntriesByUrl(item.items, accum)
})
return accum
}
通过从页面中删除 out
属性,可以防止页面被发布,但仍可使用包含指令引用。或者,您可以选择从内容目录中完全删除页面。
列出发现的组件版本
在设置 playbook 时,您可能会发现 Antora 没有发现您的某些组件版本。使用扩展,可以列出 Antora 在内容聚合期间发现的组件版本,以及它们所来自的内容源。
module.exports.register = function () {
this.once('contentAggregated', ({ contentAggregate }) => {
console.log('发现以下组件版本')
contentAggregate.forEach((bucket) => {
const sources = bucket.origins.map(({ url, refname }) => ({ url, refname }))
console.log({ name: bucket.name, version: bucket.version, files: bucket.files.length, sources })
})
})
}
如果缺少条目,则您可能需要调整 playbook 中的内容源定义。
要获取更多信息,您可以打印整个 bucket 条目。
解析附件中的属性引用
附件系列中的文件直接传递到输出站点。Antora 不会解析附件文件中的 AsciiDoc 属性引用。(另一方面,如果在启用属性替换的 AsciiDoc 页面中包含附件,则 Asciidoctor 将仅解析附件内容中的 AsciiDoc 属性引用。)您可以使用 Antora 扩展来使 Antora 在发布该文件之前解析附件文件中的属性引用。
此扩展在 contentClassified
事件期间运行,这是首次识别和分类附件文件的时候。它遍历所有附件并解析任何引用到该附件组件版本范围内的属性。如果对文件内容进行了任何更改,则将更新后的值替换虚拟文件上的内容。
module.exports.register = function () {
this.on('contentClassified', ({ contentCatalog }) => {
const componentVersionTable = contentCatalog.getComponents().reduce((componentMap, component) => {
componentMap[component.name] = component.versions.reduce((versionMap, componentVersion) => {
versionMap[componentVersion.version] = componentVersion
return versionMap
}, {})
return componentMap
}, {})
contentCatalog.findBy({ family: 'attachment' }).forEach((attachment) => {
const componentVersion = componentVersionTable[attachment.src.component][attachment.src.version]
let attributes = componentVersion.asciidoc?.attributes
if (!attributes) return
attributes = Object.entries(attributes).reduce((accum, [name, val]) => {
accum[name] = val && val.endsWith('@') ? val.slice(0, val.length - 1) : val
return accum
}, {})
let modified
const result = attachment.contents.toString().replace(/\{([\p{Alpha}\d_][\p{Alpha}\d_-]*)\}/gu, (match, name) => {
if (!(name in attributes)) return match
modified = true
let value = attributes[name]
if (value.endsWith('@')) value = value.slice(0, value.length - 1)
return value
})
if (modified) attachment.contents = Buffer.from(result)
})
})
}
此扩展仅已知适用于基于文本的附件。您可能需要修改此扩展以使其适用于二进制文件。
将文字处理附件转换为PDF
与Antora将AsciiDoc文件(.adoc)转换为HTML(.html)类似,您也可以对附件执行相同操作。此扩展在contentClassified
事件期间运行,这是首次识别和分类附件文件的时候。它会遍历所有以文字处理格式(即.docx,.odt,.fodt)保存的附件,并使用libreoffice命令(LibreOffice 服务器模式)将每个文件转换为PDF。
const fsp = require('node:fs/promises')
const ospath = require('node:path')
const { posix: path } = ospath
const { execFile } = require('node:child_process')
module.exports.register = function () {
this.once('contentClassified', async ({ playbook, contentCatalog }) => {
const docExtnames = { '.docx': true, '.fodt': true, '.odt': true }
const filesToConvert = contentCatalog.getFiles().filter(({ src }) => src.family === 'attachment' && docExtnames[src.extname])
if (!filesToConvert.length) return
const buildDirBase = ospath.join(playbook.dir, 'build/doc-to-pdf')
const convertArgs = ['--writer', '--convert-to', 'pdf']
const convertOpts = { cwd: buildDirBase, windowsHide: true }
try {
await fsp.mkdir(buildDirBase, { recursive: true })
await Promise.all(filesToConvert.map((file) => {
const sourceRelpath = `${file.src.component}-${file.src.module}-${file.out.basename}`
convertArgs.push(sourceRelpath)
return fsp.writeFile(ospath.join(buildDirBase, sourceRelpath), file.contents)
}))
await new Promise((resolve, reject) => {
execFile('libreoffice', convertArgs, convertOpts, (err, stderr, stdout) => {
if (!err) return resolve()
const splitIdx = stderr.indexOf('Usage: ')
if (~splitIdx) stderr = stderr.slice(0, splitIdx).trimEnd()
if (stderr) err.message += stderr
reject(err)
})
})
await Promise.all(filesToConvert.map((file) => {
file.out.path = path.join(file.out.dirname, (file.out.basename = file.out.basename.slice(0, -file.src.extname.length) + '.pdf'))
file.pub.url = file.pub.url.slice(0, -file.src.extname.length) + '.pdf'
const sourceRelpath = `${file.src.component}-${file.src.module}-${file.out.basename}`
return fsp.readFile(ospath.join(buildDirBase, sourceRelpath)).then((contents) => (file.contents = contents))
}))
} finally {
await fsp.rm(buildDirBase, { recursive: true, force: true })
}
})
}
通过转换文件并更新元数据,可以使用xref宏引用源文档。该引用将自动转换为生成站点中PDF的链接。