@springio/antora-extensions
Version:
Antora extensions that support the Spring documentation.
145 lines (135 loc) • 5.59 kB
JavaScript
const NavigationCatalog = require('./navigation-catalog')
const $unsafe = Symbol.for('unsafe')
// eslint-disable-next-line max-len
const LINK_RX =
/<a href="([^"]+)"(?: class="([^"]+)")?(?: title="([^"]+)")?(?: target="([^"]+)")?(?: rel="([^"]+)")?>(.+?)<\/a>/
/**
* Builds a {NavigationCatalog} from files in the navigation family that are
* stored in the content catalog.
*
* Queries the content catalog for files in the navigation family. Then uses
* the AsciiDoc Loader component to parse the source of each file into an
* Asciidoctor Document object. It then looks in each file for one or more nested
* unordered lists, which are used to build the navigation trees. It then
* combines those trees in sorted order as a navigation set, which gets
* stored in the navigation catalog by component/version pair.
*
* @memberof navigation-builder
*
* @param {ContentCatalog} [contentCatalog=undefined] - The content catalog
* that provides access to the virtual files in the site.
* @param {Object} [asciidocConfig={}] - AsciiDoc processor configuration options. Extensions are not propagated.
* Sets the relativizeResourceRefs option to false before passing to the loadAsciiDoc function.
* @param {Object} [asciidocConfig.attributes={}] - Shared AsciiDoc attributes to assign to the document.
*
* @returns {NavigationCatalog} A navigation catalog built from the navigation files in the content catalog.
*/
function buildNavigation (contentCatalog, siteAsciiDocConfig = {}) {
const { loadAsciiDoc = require('@antora/asciidoc-loader') } = this ? this.getFunctions($unsafe) : {}
const navCatalog = new NavigationCatalog()
const navAsciiDocConfig = { doctype: 'article', extensions: [], relativizeResourceRefs: false }
contentCatalog
.findBy({ family: 'nav' })
.reduce((accum, navFile) => {
const { component, version } = navFile.src
const key = version + '@' + component
const val = accum.get(key)
if (val) return new Map(accum).set(key, Object.assign({}, val, { navFiles: [...val.navFiles, navFile] }))
const componentVersion = contentCatalog.getComponentVersion(component, version)
const asciidocConfig = Object.assign({}, componentVersion.asciidoc || siteAsciiDocConfig, navAsciiDocConfig)
return new Map(accum).set(key, { component, version, componentVersion, asciidocConfig, navFiles: [navFile] })
}, new Map())
.forEach(({ component, version, componentVersion, asciidocConfig, navFiles }) => {
const trees = navFiles.reduce((accum, navFile) => {
accum.push(...loadNavigationFile(loadAsciiDoc, navFile, contentCatalog, asciidocConfig))
return accum
}, [])
componentVersion.navigation = navCatalog.addNavigation(component, version, trees)
})
return navCatalog
}
function loadNavigationFile (loadAsciiDoc, navFile, contentCatalog, asciidocConfig) {
const lists = loadAsciiDoc(navFile, contentCatalog, asciidocConfig).blocks.filter((b) => b.getContext() === 'ulist')
if (!lists.length) return []
const index = navFile.nav.index
return lists.map((list, idx) => {
const tree = buildNavigationTree(list.getTitle(), list.getItems())
tree.root = true
tree.order = idx ? parseFloat((index + idx / lists.length).toFixed(4)) : index
return tree
})
}
function getChildListItems (listItem) {
const blocks = listItem.getBlocks()
const candidate = blocks[0]
if (candidate) {
if (blocks.length === 1 && candidate.getContext() === 'ulist') {
return candidate.getItems()
} else {
let context
return blocks.reduce((accum, block) => {
if (
(context = block.getContext()) === 'ulist' ||
(context === 'open' && (block = block.getBlocks()[0]) && block.getContext() === 'ulist')
) {
accum.push(...block.getItems())
}
return accum
}, [])
}
} else {
return []
}
}
function buildNavigationTree (formattedContent, items) {
const entry = formattedContent ? partitionContent(formattedContent) : {}
if (items.length) entry.items = items.map((item) => buildNavigationTree(item.getText(), getChildListItems(item)))
return entry
}
// atomize? distill? decompose?
function partitionContent (content) {
if (~content.indexOf('<a')) {
const match = content.match(LINK_RX)
if (match) {
const [, url, role, title, target, rel, content] = match
const roles = role ? role.split(' ') : undefined
let result
if (roles && roles.includes('xref')) {
roles.splice(roles.indexOf('xref'), 1)
if (roles.includes('page')) {
roles.splice(roles.indexOf('page'), 1)
}
const hashIdx = url.indexOf('#')
if (~hashIdx) {
if (roles.includes('unresolved')) {
result = { content, url, urlType: 'internal', unresolved: true }
} else {
result = { content, url, urlType: 'internal', hash: url.substr(hashIdx) }
}
} else {
result = { content, url, urlType: 'internal' }
}
} else if (url.charAt() === '#') {
result = { content, url, urlType: 'fragment', hash: url }
} else {
result = { content, url, urlType: 'external' }
}
if (roles && roles.length) {
result.roles = roles.join(' ')
}
if (title) {
result.title = title
}
if (target) {
result.target = target
}
if (rel) {
result.rel = rel
}
return result
}
}
return { content }
}
module.exports = buildNavigation