@antora/asciidoc-loader
Version:
Loads AsciiDoc content into an Asciidoctor Document object (AST) for use in an Antora documentation pipeline.
145 lines (138 loc) • 6.82 kB
JavaScript
'use strict'
const Asciidoctor = require('./asciidoctor')
const { Extensions, LoggerManager, NullLogger } = Asciidoctor
const Opal = global.Opal
const createConverter = require('./converter/create')
const createExtensionRegistry = require('./create-extension-registry')
const LoggerAdapter = require('./logger/adapter')
const { posix: path } = require('path')
const resolveAsciiDocConfig = require('./config/resolve-asciidoc-config')
const resolveIncludeFile = require('./include/resolve-include-file')
const BLANK_LINE_BUF = Buffer.from('\n\n')
const DOCTITLE_MARKER_BUF = Buffer.from('= ')
const { EXAMPLES_DIR_TOKEN, PARTIALS_DIR_TOKEN } = require('./constants')
const EXTENSION_DSL_TYPES = Extensions.$constants(false).filter((name) => name.endsWith('Dsl'))
/**
* Loads the AsciiDoc source from the specified file into a Document object.
*
* Uses the Asciidoctor.js load API to parse the source of the file into an Asciidoctor Document object. Sets options
* and attributes that provide integration with the Antora environment. Options include a custom converter and extension
* registry to handle page references and include directives, respectively. It also assigns attributes that provide
* context either for the author (e.g., env=site) or processor (e.g., docfile).
*
* @memberof asciidoc-loader
*
* @param {File} file - The virtual file whose contents is an AsciiDoc source document.
* @param {ContentCatalog} [contentCatalog=undefined] - The catalog of all virtual content files in the site.
* @param {Object} [config={}] - AsciiDoc processor configuration options.
* @param {Object} [config.attributes={}] - Shared AsciiDoc attributes to assign to the document.
* @param {Array<Function>} [config.extensions=[]] - Self-registering AsciiDoc processor extension functions.
* @param {Boolean} [config.relativizeResourceRefs=true] - Configures the AsciiDoc processor to generate relative
* resource references (relative to the current page) instead of root relative (relative to the site root).
*
* @returns {Document} An Asciidoctor Document object created from the source of the specified file.
*/
function loadAsciiDoc (file, contentCatalog = undefined, config = {}) {
const { family, relative, extname = path.extname(relative) } = file.src
const { moduleRootPath = '' } = file.pub || {}
const intrinsicAttrs = {
docname: (family === 'nav' ? 'nav$' : '') + relative.substr(0, relative.length - extname.length),
docfile: file.path,
// NOTE docdir implicitly sets base_dir on document; Opal only expands value to absolute path if it starts with ./
docdir: file.dirname,
docfilesuffix: extname,
imagesdir: path.join(moduleRootPath, '_images'),
attachmentsdir: path.join(moduleRootPath, '_attachments'),
examplesdir: EXAMPLES_DIR_TOKEN,
partialsdir: PARTIALS_DIR_TOKEN,
}
const attributes = Object.assign(
family === 'page' ? { 'page-partial': '@' } : {},
config.attributes,
intrinsicAttrs,
computePageAttrs(file.src, contentCatalog)
)
const extensionRegistry = createExtensionRegistry(Asciidoctor, {
onInclude: contentCatalog
? (doc, target, cursor) => resolveIncludeFile(target, file, cursor, contentCatalog)
: () => undefined,
})
const extensions = config.extensions || []
LoggerManager.setLogger(LoggerAdapter.logger.noop ? NullLogger.$new() : LoggerAdapter.$new(file.src))
if (extensions.length) extensions.forEach((ext) => ext.register(extensionRegistry, { file, contentCatalog, config }))
const opts = { attributes, extension_registry: extensionRegistry, safe: 'safe' }
if (config.doctype) opts.doctype = config.doctype
if (config.sourcemap) opts.sourcemap = true
let contents = file.contents
if (config.headerOnly) {
opts.parse_header_only = true
const firstBlankLineIdx = contents.indexOf(BLANK_LINE_BUF)
if (~firstBlankLineIdx) {
const partialContents = contents.slice(0, firstBlankLineIdx)
const doctitleIdx = partialContents.indexOf(DOCTITLE_MARKER_BUF)
if (!doctitleIdx || partialContents[doctitleIdx - 1] === 10) contents = partialContents
}
} else if (contentCatalog) {
attributes.relfilesuffix = '.adoc' // NOTE relfilesuffix must be set for page-to-page xrefs to work correctly
opts.converter = createConverter(file, contentCatalog, config)
}
try {
return Asciidoctor.load(contents.toString(), opts)
} finally {
if (extensions.length) {
// low-level operation to free objects from memory that have been weaved into an extension DSL module
EXTENSION_DSL_TYPES.forEach((type) => (Opal.const_get_local(Extensions, type).$$iclasses.length = 0))
}
}
}
function computePageAttrs ({ component, version, module: module_, relative, origin, editUrl }, contentCatalog) {
const attrs = {}
attrs['page-component-name'] = component
attrs['page-component-version'] = attrs['page-version'] = version
if ((component = contentCatalog?.getComponent(component))) {
attrs['page-component-title'] = component.title
const componentVersion = component.versions.find((it) => it.version === version)
if (componentVersion) {
attrs['page-component-display-version'] = componentVersion.displayVersion
const latestVersion = component.latest.version
if (version === latestVersion) attrs['page-component-version-is-latest'] = ''
attrs['page-component-latest-version'] = latestVersion
}
}
attrs['page-module'] = module_
attrs['page-relative-src-path'] = relative
if (editUrl) attrs['page-edit-url'] = editUrl
if (origin) {
attrs['page-origin-type'] = origin.type
attrs['page-origin-url'] = origin.url
attrs['page-origin-start-path'] = origin.startPath
attrs['page-origin-reftype'] = origin.reftype
attrs['page-origin-refname'] = attrs['page-origin-' + origin.reftype] = origin.refname
if (origin.worktree) {
attrs['page-origin-worktree'] = origin.worktree
attrs['page-origin-refhash'] = '(worktree)'
} else {
attrs['page-origin-refhash'] = origin.refhash
}
if (origin.private) attrs['page-origin-private'] = ''
}
return attrs
}
function extractAsciiDocMetadata (doc) {
const metadata = { attributes: doc.getAttributes() }
if (doc.hasHeader()) {
const doctitle = (metadata.doctitle = doc.getDocumentTitle())
const xreftext = (metadata.xreftext = doc.$reftext().$to_s() || doctitle)
const navtitle = doc.getAttribute('navtitle')
metadata.navtitle = navtitle ? doc.$apply_reftext_subs(navtitle) : xreftext
const id = doc.getId()
if (id) metadata.id = id
}
return metadata
}
module.exports = Object.assign(loadAsciiDoc, {
loadAsciiDoc,
extractAsciiDocMetadata,
resolveAsciiDocConfig,
resolveConfig: resolveAsciiDocConfig, // @deprecated scheduled to be removed in Antora 4
})