UNPKG

@springio/antora-extensions

Version:

Antora extensions that support the Spring documentation.

145 lines (135 loc) 5.59 kB
'use strict' 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