Skip to content

Commit 3df0600

Browse files
committed
Override buildNavigation Extension
Closes gh-28
1 parent 25a507c commit 3df0600

10 files changed

+2886
-9
lines changed

README.adoc

+26
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,32 @@ The extension also maps the latest release to a URL that leverages only the `${m
117117
NOTE: Filtering is intentionally performed on the tags rather than versions because the Spring team calculates versions by extracting the information from the Java based build.
118118
The amount of time to calculate the version is small, but adds up with lots of tags, and we'd like to avoid this computational cost on tags that are not being used.
119119

120+
121+
=== override-navigation-builder-extension
122+
123+
*require name:* @springio/antora-extensions/override-navigation-builder-extension
124+
125+
IMPORTANT: Be sure to register this extension under the `antora.extensions` key in the playbook, not the `asciidoc.extensions` key!
126+
127+
The purpose of this extension is override the navigation builder to work around https://gitlab.com/antora/antora/-/issues/701
128+
129+
The summary is that this allows xref entries in the navigation to propagate roles to the model.
130+
The following will have a model that contains the role `custom`.
131+
132+
[source,asciidoc]
133+
----
134+
* xref::index.adoc[Optional Text, role=custom]
135+
----
136+
137+
The following will have a model that contains the roles `a b`.
138+
139+
[source,asciidoc]
140+
----
141+
* xref::index.adoc[Optional Text, role=a b]
142+
----
143+
144+
Additional attributes `title`, `target`, and `rel` are also propagated to the model.
145+
120146
=== Partial Build
121147

122148
*require name:* @springio/antora-extensions/partial-build-extension
+144
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
'use strict'
2+
3+
const NavigationCatalog = require('./navigation-catalog')
4+
5+
const $unsafe = Symbol.for('unsafe')
6+
7+
// eslint-disable-next-line max-len
8+
const LINK_RX =
9+
/<a href="([^"]+)"(?: class="([^"]+)")?(?: title="([^"]+)")?(?: target="([^"]+)")?(?: rel="([^"]+)")?>(.+?)<\/a>/
10+
11+
/**
12+
* Builds a {NavigationCatalog} from files in the navigation family that are
13+
* stored in the content catalog.
14+
*
15+
* Queries the content catalog for files in the navigation family. Then uses
16+
* the AsciiDoc Loader component to parse the source of each file into an
17+
* Asciidoctor Document object. It then looks in each file for one or more nested
18+
* unordered lists, which are used to build the navigation trees. It then
19+
* combines those trees in sorted order as a navigation set, which gets
20+
* stored in the navigation catalog by component/version pair.
21+
*
22+
* @memberof navigation-builder
23+
*
24+
* @param {ContentCatalog} [contentCatalog=undefined] - The content catalog
25+
* that provides access to the virtual files in the site.
26+
* @param {Object} [asciidocConfig={}] - AsciiDoc processor configuration options. Extensions are not propagated.
27+
* Sets the relativizeResourceRefs option to false before passing to the loadAsciiDoc function.
28+
* @param {Object} [asciidocConfig.attributes={}] - Shared AsciiDoc attributes to assign to the document.
29+
*
30+
* @returns {NavigationCatalog} A navigation catalog built from the navigation files in the content catalog.
31+
*/
32+
function buildNavigation (contentCatalog, siteAsciiDocConfig = {}) {
33+
const { loadAsciiDoc = require('@antora/asciidoc-loader') } = this ? this.getFunctions($unsafe) : {}
34+
const navCatalog = new NavigationCatalog()
35+
const navAsciiDocConfig = { doctype: 'article', extensions: [], relativizeResourceRefs: false }
36+
contentCatalog
37+
.findBy({ family: 'nav' })
38+
.reduce((accum, navFile) => {
39+
const { component, version } = navFile.src
40+
const key = version + '@' + component
41+
const val = accum.get(key)
42+
if (val) return new Map(accum).set(key, Object.assign({}, val, { navFiles: [...val.navFiles, navFile] }))
43+
const componentVersion = contentCatalog.getComponentVersion(component, version)
44+
const asciidocConfig = Object.assign({}, componentVersion.asciidoc || siteAsciiDocConfig, navAsciiDocConfig)
45+
return new Map(accum).set(key, { component, version, componentVersion, asciidocConfig, navFiles: [navFile] })
46+
}, new Map())
47+
.forEach(({ component, version, componentVersion, asciidocConfig, navFiles }) => {
48+
const trees = navFiles.reduce((accum, navFile) => {
49+
accum.push(...loadNavigationFile(loadAsciiDoc, navFile, contentCatalog, asciidocConfig))
50+
return accum
51+
}, [])
52+
componentVersion.navigation = navCatalog.addNavigation(component, version, trees)
53+
})
54+
return navCatalog
55+
}
56+
57+
function loadNavigationFile (loadAsciiDoc, navFile, contentCatalog, asciidocConfig) {
58+
const lists = loadAsciiDoc(navFile, contentCatalog, asciidocConfig).blocks.filter((b) => b.getContext() === 'ulist')
59+
if (!lists.length) return []
60+
const index = navFile.nav.index
61+
return lists.map((list, idx) => {
62+
const tree = buildNavigationTree(list.getTitle(), list.getItems())
63+
tree.root = true
64+
tree.order = idx ? parseFloat((index + idx / lists.length).toFixed(4)) : index
65+
return tree
66+
})
67+
}
68+
69+
function getChildListItems (listItem) {
70+
const blocks = listItem.getBlocks()
71+
const candidate = blocks[0]
72+
if (candidate) {
73+
if (blocks.length === 1 && candidate.getContext() === 'ulist') {
74+
return candidate.getItems()
75+
} else {
76+
let context
77+
return blocks.reduce((accum, block) => {
78+
if (
79+
(context = block.getContext()) === 'ulist' ||
80+
(context === 'open' && (block = block.getBlocks()[0]) && block.getContext() === 'ulist')
81+
) {
82+
accum.push(...block.getItems())
83+
}
84+
return accum
85+
}, [])
86+
}
87+
} else {
88+
return []
89+
}
90+
}
91+
92+
function buildNavigationTree (formattedContent, items) {
93+
const entry = formattedContent ? partitionContent(formattedContent) : {}
94+
if (items.length) entry.items = items.map((item) => buildNavigationTree(item.getText(), getChildListItems(item)))
95+
return entry
96+
}
97+
98+
// atomize? distill? decompose?
99+
function partitionContent (content) {
100+
if (~content.indexOf('<a')) {
101+
const match = content.match(LINK_RX)
102+
if (match) {
103+
const [, url, role, title, target, rel, content] = match
104+
const roles = role ? role.split(' ') : undefined
105+
let result
106+
if (roles && roles.includes('xref')) {
107+
roles.splice(roles.indexOf('xref'), 1)
108+
if (roles.includes('page')) {
109+
roles.splice(roles.indexOf('page'), 1)
110+
}
111+
const hashIdx = url.indexOf('#')
112+
if (~hashIdx) {
113+
if (roles.includes('unresolved')) {
114+
result = { content, url, urlType: 'internal', unresolved: true }
115+
} else {
116+
result = { content, url, urlType: 'internal', hash: url.substr(hashIdx) }
117+
}
118+
} else {
119+
result = { content, url, urlType: 'internal' }
120+
}
121+
} else if (url.charAt() === '#') {
122+
result = { content, url, urlType: 'fragment', hash: url }
123+
} else {
124+
result = { content, url, urlType: 'external' }
125+
}
126+
if (roles && roles.length) {
127+
result.roles = roles.join(' ')
128+
}
129+
if (title) {
130+
result.title = title
131+
}
132+
if (target) {
133+
result.target = target
134+
}
135+
if (rel) {
136+
result.rel = rel
137+
}
138+
return result
139+
}
140+
}
141+
return { content }
142+
}
143+
144+
module.exports = buildNavigation
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
'use strict'
2+
3+
const $sets = Symbol('sets')
4+
5+
class NavigationCatalog {
6+
constructor () {
7+
this[$sets] = {}
8+
}
9+
10+
addTree (component, version, tree) {
11+
const key = generateKey(component, version)
12+
const navigation = this[$sets][key] || (this[$sets][key] = [])
13+
// NOTE retain order on insert
14+
const insertIdx = navigation.findIndex((candidate) => candidate.order >= tree.order)
15+
~insertIdx ? navigation.splice(insertIdx, 0, tree) : navigation.push(tree)
16+
return navigation
17+
}
18+
19+
addNavigation (component, version, trees) {
20+
return (this[$sets][generateKey(component, version)] = trees.sort((a, b) => a.order - b.order))
21+
}
22+
23+
getNavigation (component, version) {
24+
return this[$sets][generateKey(component, version)]
25+
}
26+
}
27+
28+
function generateKey (component, version) {
29+
return version + '@' + component
30+
}
31+
32+
module.exports = NavigationCatalog
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
'use strict'
2+
3+
const buildNavigation = require('./build-navigation')
4+
5+
module.exports.register = function () {
6+
this.replaceFunctions({
7+
buildNavigation (contentCatalog, siteAsciiDocConfig) {
8+
return buildNavigation.call(this, contentCatalog, siteAsciiDocConfig)
9+
},
10+
})
11+
}

0 commit comments

Comments
 (0)