@baiyibai-antora/adf-extension 1.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.adoc +5 -0
- package/lib/convert-pages.js +303 -0
- package/lib/index.js +35 -0
- package/lib/load-config.js +45 -0
- package/lib/nav.js +54 -0
- package/package.json +42 -0
package/README.adoc
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
= @baiyibai-antora/adf-extension
|
|
2
|
+
|
|
3
|
+
Antora extension that converts every publishable page of an Antora site to Confluence-native ADF JSON during a normal site build, plus a `manifest.json` publish contract for `@baiyibai-antora/confluence-publisher`.
|
|
4
|
+
|
|
5
|
+
Full documentation lives in the repository https://gitlab.com/baiyibai-antora/confluora/-/blob/main/README.adoc[README].
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { createHash } = require('node:crypto')
|
|
4
|
+
const fsp = require('node:fs/promises')
|
|
5
|
+
const ospath = require('node:path')
|
|
6
|
+
const { posix: path } = require('node:path')
|
|
7
|
+
const { convert } = require('@baiyibai-antora/asciidoc-adf-converter')
|
|
8
|
+
const extractNav = require('./nav')
|
|
9
|
+
|
|
10
|
+
const pageKey = (src) => [src.component, src.version, src.module, src.relative].join(':')
|
|
11
|
+
const md5 = (buf) => createHash('md5').update(buf).digest('hex')
|
|
12
|
+
const sha1 = (str) => createHash('sha1').update(str).digest('hex')
|
|
13
|
+
|
|
14
|
+
// Confluence page titles are plain text; doctitles and nav link texts can
|
|
15
|
+
// carry inline markup, entities, and (when metadata was extracted without
|
|
16
|
+
// extensions loaded) an unconsumed leading macro escape.
|
|
17
|
+
const ENTITIES = { '&': '&', '<': '<', '>': '>', '"': '"', ''': "'", ' ': ' ', '…': '…' }
|
|
18
|
+
const plainText = (html) =>
|
|
19
|
+
html &&
|
|
20
|
+
html
|
|
21
|
+
.replace(/<[^>]+>/g, '')
|
|
22
|
+
.replace(/&#x([0-9a-f]+);/gi, (_, hex) => String.fromCodePoint(parseInt(hex, 16)))
|
|
23
|
+
.replace(/&#(\d+);/g, (_, num) => String.fromCodePoint(Number(num)))
|
|
24
|
+
.replace(/&[a-z]+;/gi, (ref) => ENTITIES[ref] || ref)
|
|
25
|
+
.replace(/[-]/g, '') // zero-width chars from AsciiDoc word joiners
|
|
26
|
+
.replace(/^\\(?=[a-z])/i, '')
|
|
27
|
+
.trim()
|
|
28
|
+
|
|
29
|
+
// HTML-pipeline extensions (e.g. list-of-site) leave placeholder divs in the
|
|
30
|
+
// AsciiDoc-derived pass blocks and inject the real content into the page's
|
|
31
|
+
// converted HTML at documentsConverted — which has already run by the time we
|
|
32
|
+
// re-load the source. Splice the generated markup back into the placeholders.
|
|
33
|
+
const PLACEHOLDER_RX = /<div\s+class="list-of-site-placeholder[^"]*"\s+data-element="([^"]+)">\s*<\/div>/g
|
|
34
|
+
function createPassTransform (page) {
|
|
35
|
+
const html = page.contents && page.contents.toString()
|
|
36
|
+
if (!html || !html.includes('list-of')) return null
|
|
37
|
+
const used = {}
|
|
38
|
+
return (passHtml) =>
|
|
39
|
+
passHtml.replace(PLACEHOLDER_RX, (match, element) => {
|
|
40
|
+
const generated =
|
|
41
|
+
html.match(new RegExp(`<ul class="list-of-${element}[^"]*">[\\s\\S]*?</ul>`, 'g')) || []
|
|
42
|
+
const idx = (used[element] = (used[element] ?? -1) + 1)
|
|
43
|
+
return generated[idx] || ''
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Never surface a raw source path as a page title: fall back to the nav link
|
|
48
|
+
// text, then to a titleized filename stem.
|
|
49
|
+
function pageTitle (page, key, navTitles) {
|
|
50
|
+
const doctitle = plainText(page.title)
|
|
51
|
+
if (doctitle) return doctitle
|
|
52
|
+
const navTitle = plainText(navTitles.get(key))
|
|
53
|
+
if (navTitle) return navTitle
|
|
54
|
+
const stem = page.src.relative.replace(/\.adoc$/, '').split('/').pop().replace(/[-_]+/g, ' ')
|
|
55
|
+
return stem.charAt(0).toUpperCase() + stem.slice(1)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
module.exports = async function convertPages ({
|
|
59
|
+
playbook,
|
|
60
|
+
contentCatalog,
|
|
61
|
+
navigationCatalog,
|
|
62
|
+
loadAsciiDoc,
|
|
63
|
+
config,
|
|
64
|
+
logger,
|
|
65
|
+
}) {
|
|
66
|
+
const outputDir = ospath.resolve(playbook.dir || '.', config.outputDir || path.join('build', 'adf'))
|
|
67
|
+
const included = buildVersionFilter(contentCatalog, config)
|
|
68
|
+
|
|
69
|
+
const asciidocConfigs = new Map()
|
|
70
|
+
for (const component of contentCatalog.getComponents()) {
|
|
71
|
+
for (const { version, asciidoc } of component.versions) {
|
|
72
|
+
asciidocConfigs.set(`${component.name}@${version}`, asciidoc)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const pages = contentCatalog.getPages(
|
|
77
|
+
(page) => page.out && page.src.contents && included(page.src, pageKey(page.src))
|
|
78
|
+
)
|
|
79
|
+
const urlIndex = new Map(pages.map((page) => [page.pub.url, page]))
|
|
80
|
+
const { nav, parents, order, navTitles } = extractNav(contentCatalog, navigationCatalog, urlIndex, pageKey, included)
|
|
81
|
+
|
|
82
|
+
// Nav parity: pages absent from the Antora navigation are orphans — they
|
|
83
|
+
// never appear in the site's sidebar, so by default they don't publish
|
|
84
|
+
// either (links to them fall back to the static site). Component start
|
|
85
|
+
// pages always publish; set orphans: include to publish everything.
|
|
86
|
+
const startKeys = new Set(nav.map((entry) => entry.startKey).filter(Boolean))
|
|
87
|
+
let publishable = pages
|
|
88
|
+
if (config.orphans !== 'include') {
|
|
89
|
+
publishable = pages.filter((page) => {
|
|
90
|
+
const key = pageKey(page.src)
|
|
91
|
+
if (order.has(key) || startKeys.has(key)) return true
|
|
92
|
+
logger.info(`${key}: not in the Antora navigation; skipping (set orphans: include to publish)`)
|
|
93
|
+
return false
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const writtenAttachments = new Map() // output-relative path -> contents
|
|
98
|
+
const manifestPages = []
|
|
99
|
+
|
|
100
|
+
for (const page of publishable) {
|
|
101
|
+
const key = pageKey(page.src)
|
|
102
|
+
const asciidocConfig = asciidocConfigs.get(`${page.src.component}@${page.src.version}`) || {}
|
|
103
|
+
const doc = loadAsciiDoc(
|
|
104
|
+
{
|
|
105
|
+
contents: page.src.contents,
|
|
106
|
+
src: page.src,
|
|
107
|
+
pub: page.pub,
|
|
108
|
+
out: page.out,
|
|
109
|
+
path: page.path,
|
|
110
|
+
dirname: ospath.dirname(page.path),
|
|
111
|
+
},
|
|
112
|
+
contentCatalog,
|
|
113
|
+
// Root-relative refs map directly onto the pub.url page index.
|
|
114
|
+
Object.assign({}, asciidocConfig, { relativizeResourceRefs: false })
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
const attachments = []
|
|
118
|
+
const attachmentsBySource = new Map()
|
|
119
|
+
const xrefs = []
|
|
120
|
+
|
|
121
|
+
const result = convert(doc, {
|
|
122
|
+
transformPassContent: createPassTransform(page),
|
|
123
|
+
resolveImage: (target) => {
|
|
124
|
+
if (/^(https?|data):/.test(target)) return { url: target }
|
|
125
|
+
const image = contentCatalog.resolveResource(target, page.src, 'image', ['image'])
|
|
126
|
+
if (!image || !image.contents) return null
|
|
127
|
+
const sourceKey = pageKey(image.src)
|
|
128
|
+
let entry = attachmentsBySource.get(sourceKey)
|
|
129
|
+
if (!entry) {
|
|
130
|
+
const hash = md5(image.contents)
|
|
131
|
+
const relPath = path.join(
|
|
132
|
+
'attachments',
|
|
133
|
+
image.src.component,
|
|
134
|
+
image.src.version,
|
|
135
|
+
image.src.module,
|
|
136
|
+
image.src.relative
|
|
137
|
+
)
|
|
138
|
+
writtenAttachments.set(relPath, image.contents)
|
|
139
|
+
entry = {
|
|
140
|
+
placeholderId: hash,
|
|
141
|
+
path: relPath,
|
|
142
|
+
filename: path.basename(image.src.relative),
|
|
143
|
+
md5: hash,
|
|
144
|
+
}
|
|
145
|
+
attachmentsBySource.set(sourceKey, entry)
|
|
146
|
+
attachments.push(entry)
|
|
147
|
+
}
|
|
148
|
+
return { id: entry.placeholderId, collection: '' }
|
|
149
|
+
},
|
|
150
|
+
resolveXref: (href, attribs = {}) => {
|
|
151
|
+
if (!href) return { href }
|
|
152
|
+
// Extension-generated HTML (e.g. list-of-site) links relative to the
|
|
153
|
+
// page; normalize to root-relative so the publisher can map it.
|
|
154
|
+
let resolved = href
|
|
155
|
+
if (href.charAt(0) !== '/' && !/^[a-z][a-z0-9+.-]*:/i.test(href) && href.charAt(0) !== '#') {
|
|
156
|
+
resolved = path.resolve(path.dirname(page.pub.url), href)
|
|
157
|
+
}
|
|
158
|
+
if (resolved.charAt(0) === '/') {
|
|
159
|
+
const [urlPath, fragment] = resolved.split('#')
|
|
160
|
+
const targetPage = urlIndex.get(urlPath)
|
|
161
|
+
const xref = { href: resolved, targetKey: targetPage ? pageKey(targetPage.src) : null }
|
|
162
|
+
if (fragment) xref.fragment = fragment
|
|
163
|
+
if (!targetPage && /(^| )xref( |$)/.test(attribs.class || '')) {
|
|
164
|
+
logger.warn(`${key}: xref "${resolved}" does not resolve to a published page`)
|
|
165
|
+
}
|
|
166
|
+
xrefs.push(xref)
|
|
167
|
+
return { href: resolved }
|
|
168
|
+
}
|
|
169
|
+
return { href }
|
|
170
|
+
},
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
for (const warning of result.warnings) logger.warn(`${key}: ${warning}`)
|
|
174
|
+
|
|
175
|
+
const adfJson = JSON.stringify(result.adf, null, 2) + '\n'
|
|
176
|
+
const adfPath = path.join(
|
|
177
|
+
'pages',
|
|
178
|
+
page.src.component,
|
|
179
|
+
page.src.version,
|
|
180
|
+
page.src.module,
|
|
181
|
+
page.src.relative.replace(/\.adoc$/, '.adf.json')
|
|
182
|
+
)
|
|
183
|
+
const outFile = ospath.join(outputDir, ...adfPath.split('/'))
|
|
184
|
+
await fsp.mkdir(ospath.dirname(outFile), { recursive: true })
|
|
185
|
+
await fsp.writeFile(outFile, adfJson)
|
|
186
|
+
|
|
187
|
+
manifestPages.push({
|
|
188
|
+
key,
|
|
189
|
+
title: pageTitle(page, key, navTitles),
|
|
190
|
+
adfPath,
|
|
191
|
+
url: page.pub.url,
|
|
192
|
+
// The page's source in its git repository, as a VIEW url — Antora's
|
|
193
|
+
// editUrl points at the edit form, which forces a forge login; the
|
|
194
|
+
// blob/view page is public and still offers Edit to those who can.
|
|
195
|
+
// Local-worktree builds yield file:// urls — never publishable.
|
|
196
|
+
sourceUrl:
|
|
197
|
+
page.src.editUrl && /^https?:/.test(page.src.editUrl)
|
|
198
|
+
? page.src.editUrl.replace(/\/edit\//, '/blob/')
|
|
199
|
+
: null,
|
|
200
|
+
navOrder: order.has(key) ? order.get(key) : null,
|
|
201
|
+
parentKey: parents.get(key) || null,
|
|
202
|
+
xrefs,
|
|
203
|
+
attachments,
|
|
204
|
+
anchors: result.anchors,
|
|
205
|
+
contentHash: sha1(adfJson),
|
|
206
|
+
})
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
for (const [relPath, contents] of writtenAttachments) {
|
|
210
|
+
const outFile = ospath.join(outputDir, ...relPath.split('/'))
|
|
211
|
+
await fsp.mkdir(ospath.dirname(outFile), { recursive: true })
|
|
212
|
+
await fsp.writeFile(outFile, contents)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Stable page order: nav pages first (nav order), then the rest by key.
|
|
216
|
+
manifestPages.sort((a, b) => {
|
|
217
|
+
if (a.navOrder != null && b.navOrder != null) return a.navOrder - b.navOrder
|
|
218
|
+
if (a.navOrder != null) return -1
|
|
219
|
+
if (b.navOrder != null) return 1
|
|
220
|
+
return a.key < b.key ? -1 : a.key > b.key ? 1 : 0
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
logger.info(`wrote ${manifestPages.length} ADF page(s) and ${writtenAttachments.size} attachment(s) to ${outputDir}`)
|
|
224
|
+
// The manifest itself is written by finalizeManifest (on beforePublish), so
|
|
225
|
+
// assembler-produced exports (family "export") are collected regardless of
|
|
226
|
+
// extension registration order.
|
|
227
|
+
return { outputDir, manifestPages, nav, included }
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Component/version scoping: `components` limits to the listed component
|
|
231
|
+
// names; `versions: latest` keeps only each component's latest version;
|
|
232
|
+
// `exclude` is a list of globs matched against component names and full page
|
|
233
|
+
// keys (component:version:module:relative).
|
|
234
|
+
function buildVersionFilter (contentCatalog, config) {
|
|
235
|
+
const components = Array.isArray(config.components) && config.components.length ? config.components : null
|
|
236
|
+
const latestOnly = config.versions === 'latest'
|
|
237
|
+
const latest = new Map(contentCatalog.getComponents().map((c) => [c.name, c.latest && c.latest.version]))
|
|
238
|
+
const excludes = (Array.isArray(config.exclude) ? config.exclude : []).map(globToRx)
|
|
239
|
+
return (src, key) => {
|
|
240
|
+
if (components && !components.includes(src.component)) return false
|
|
241
|
+
if (latestOnly && latest.get(src.component) !== src.version) return false
|
|
242
|
+
for (const rx of excludes) {
|
|
243
|
+
if (rx.test(src.component)) return false
|
|
244
|
+
if (key && rx.test(key)) return false
|
|
245
|
+
}
|
|
246
|
+
return true
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function globToRx (pattern) {
|
|
251
|
+
const escaped = String(pattern).replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*').replace(/\?/g, '.')
|
|
252
|
+
return new RegExp(`^${escaped}$`)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* beforePublish hook: collect assembler exports (family "export", e.g. PDFs)
|
|
257
|
+
* scoped to the included component versions, copy their bytes, and write the
|
|
258
|
+
* final manifest.json.
|
|
259
|
+
*/
|
|
260
|
+
async function finalizeManifest ({ playbook, contentCatalog, state, logger }) {
|
|
261
|
+
const { outputDir, manifestPages, nav, included } = state
|
|
262
|
+
const exports_ = []
|
|
263
|
+
for (const file of contentCatalog.getFiles()) {
|
|
264
|
+
if (!file.src || file.src.family !== 'export' || !file.out || !file.contents) continue
|
|
265
|
+
if (!included(file.src)) continue
|
|
266
|
+
// Assembler exports carry their bytes as a lazy stream, not a Buffer.
|
|
267
|
+
const contents = Buffer.isBuffer(file.contents) ? file.contents : await bufferStream(file.contents)
|
|
268
|
+
// A generic "index.pdf" makes a poor download; qualify it.
|
|
269
|
+
let filename = path.basename(file.out.path)
|
|
270
|
+
if (filename.startsWith('index.')) filename = `${file.src.component}-${file.src.version}${filename.slice(5)}`
|
|
271
|
+
const relPath = path.join('exports', file.src.component, file.src.version, filename)
|
|
272
|
+
const outFile = ospath.join(outputDir, ...relPath.split('/'))
|
|
273
|
+
await fsp.mkdir(ospath.dirname(outFile), { recursive: true })
|
|
274
|
+
await fsp.writeFile(outFile, contents)
|
|
275
|
+
exports_.push({
|
|
276
|
+
component: file.src.component,
|
|
277
|
+
version: file.src.version,
|
|
278
|
+
filename,
|
|
279
|
+
path: relPath,
|
|
280
|
+
md5: md5(contents),
|
|
281
|
+
})
|
|
282
|
+
logger.info(`collected export ${filename} (${file.src.component}@${file.src.version})`)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const manifest = {
|
|
286
|
+
version: 1,
|
|
287
|
+
site: { title: playbook.site.title || null, url: playbook.site.url || null },
|
|
288
|
+
pages: manifestPages,
|
|
289
|
+
nav,
|
|
290
|
+
exports: exports_,
|
|
291
|
+
}
|
|
292
|
+
await fsp.writeFile(ospath.join(outputDir, 'manifest.json'), JSON.stringify(manifest, null, 2) + '\n')
|
|
293
|
+
logger.info(`wrote manifest: ${manifestPages.length} page(s), ${exports_.length} export(s)`)
|
|
294
|
+
return manifest
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
async function bufferStream (stream) {
|
|
298
|
+
const chunks = []
|
|
299
|
+
for await (const chunk of stream) chunks.push(chunk)
|
|
300
|
+
return Buffer.concat(chunks)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
module.exports.finalizeManifest = finalizeManifest
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const convertPages = require('./convert-pages')
|
|
4
|
+
const { finalizeManifest } = require('./convert-pages')
|
|
5
|
+
const loadConfig = require('./load-config')
|
|
6
|
+
|
|
7
|
+
module.exports.register = function ({ config = {} }) {
|
|
8
|
+
const logger = this.getLogger('adf-extension')
|
|
9
|
+
let state
|
|
10
|
+
|
|
11
|
+
// The AsciiDoc source of each page must survive HTML conversion so it can
|
|
12
|
+
// be re-loaded and walked for ADF output.
|
|
13
|
+
this.once('beforeProcess', ({ siteAsciiDocConfig }) => {
|
|
14
|
+
siteAsciiDocConfig.keepSource = true
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
this.once('navigationBuilt', async ({ playbook, contentCatalog, navigationCatalog }) => {
|
|
18
|
+
const loadAsciiDoc = this.require('@antora/asciidoc-loader')
|
|
19
|
+
const resolvedConfig = loadConfig(playbook, config, logger)
|
|
20
|
+
state = await convertPages({
|
|
21
|
+
playbook,
|
|
22
|
+
contentCatalog,
|
|
23
|
+
navigationCatalog,
|
|
24
|
+
loadAsciiDoc,
|
|
25
|
+
config: resolvedConfig,
|
|
26
|
+
logger,
|
|
27
|
+
})
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
// Deferred to beforePublish so assembler exports (PDFs etc., family
|
|
31
|
+
// "export") exist in the catalog no matter the extension order.
|
|
32
|
+
this.once('beforePublish', async ({ playbook, contentCatalog }) => {
|
|
33
|
+
if (state) await finalizeManifest({ playbook, contentCatalog, state, logger })
|
|
34
|
+
})
|
|
35
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs')
|
|
4
|
+
const ospath = require('node:path')
|
|
5
|
+
const yaml = require('js-yaml')
|
|
6
|
+
|
|
7
|
+
// Confluence-specific site configuration lives in a YAML file next to the
|
|
8
|
+
// playbook (default: confluence.yml, override with the config_file key), so
|
|
9
|
+
// publishing concerns stay out of the Antora playbook proper:
|
|
10
|
+
//
|
|
11
|
+
// nav_exclude: # not published to Confluence (globs vs component
|
|
12
|
+
// - my-presentation # names / page keys); the sources stay in the
|
|
13
|
+
// versions: latest # Antora catalog, so includes and xrefs resolve —
|
|
14
|
+
// components: [docs] # links to nav-excluded pages fall back to the
|
|
15
|
+
// output_dir: ./build/adf # static site url. `exclude` is an alias.
|
|
16
|
+
//
|
|
17
|
+
// Keys given directly on the extension entry in the playbook win.
|
|
18
|
+
|
|
19
|
+
function loadConfig (playbook, config, logger) {
|
|
20
|
+
const filename = config.configFile || 'confluence.yml'
|
|
21
|
+
const configPath = ospath.resolve(playbook.dir || '.', filename)
|
|
22
|
+
if (!fs.existsSync(configPath)) {
|
|
23
|
+
if (config.configFile) logger.warn(`confluence config file not found: ${configPath}`)
|
|
24
|
+
return config
|
|
25
|
+
}
|
|
26
|
+
let fileConfig
|
|
27
|
+
try {
|
|
28
|
+
fileConfig = yaml.load(fs.readFileSync(configPath, 'utf8')) || {}
|
|
29
|
+
} catch (err) {
|
|
30
|
+
logger.warn(`could not parse ${configPath}: ${err.message}`)
|
|
31
|
+
return config
|
|
32
|
+
}
|
|
33
|
+
const normalized = {}
|
|
34
|
+
if (fileConfig.exclude || fileConfig.nav_exclude) {
|
|
35
|
+
normalized.exclude = [...(fileConfig.exclude || []), ...(fileConfig.nav_exclude || [])]
|
|
36
|
+
}
|
|
37
|
+
if (fileConfig.versions) normalized.versions = fileConfig.versions
|
|
38
|
+
if (fileConfig.orphans) normalized.orphans = fileConfig.orphans
|
|
39
|
+
if (fileConfig.components) normalized.components = fileConfig.components
|
|
40
|
+
if (fileConfig.output_dir) normalized.outputDir = fileConfig.output_dir
|
|
41
|
+
logger.info(`loaded confluence config from ${configPath}`)
|
|
42
|
+
return { ...normalized, ...config }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
module.exports = loadConfig
|
package/lib/nav.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// Extracts the navigation tree per component version, resolving internal nav
|
|
4
|
+
// URLs back to page keys. Returns the manifest nav plus page parent/order
|
|
5
|
+
// maps derived from a depth-first walk.
|
|
6
|
+
|
|
7
|
+
module.exports = function extractNav (contentCatalog, navigationCatalog, urlIndex, pageKey, included = () => true) {
|
|
8
|
+
const nav = []
|
|
9
|
+
const parents = new Map()
|
|
10
|
+
const order = new Map()
|
|
11
|
+
const navTitles = new Map()
|
|
12
|
+
let counter = 0
|
|
13
|
+
|
|
14
|
+
const visit = (item, parentKey) => {
|
|
15
|
+
let key = null
|
|
16
|
+
if (item.urlType === 'internal' && item.url) {
|
|
17
|
+
const page = urlIndex.get(item.url.split('#')[0])
|
|
18
|
+
if (page) {
|
|
19
|
+
key = pageKey(page.src)
|
|
20
|
+
if (!parents.has(key)) {
|
|
21
|
+
parents.set(key, parentKey)
|
|
22
|
+
order.set(key, counter++)
|
|
23
|
+
if (item.content) navTitles.set(key, item.content)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const entry = { title: item.content || null, key }
|
|
28
|
+
const children = (item.items || []).map((child) => visit(child, key || parentKey))
|
|
29
|
+
if (children.length) entry.children = children
|
|
30
|
+
return entry
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
for (const component of contentCatalog.getComponents()) {
|
|
34
|
+
for (const { version, displayVersion, title, url } of component.versions) {
|
|
35
|
+
if (!included({ component: component.name, version })) continue
|
|
36
|
+
const menus = navigationCatalog.getNavigation(component.name, version) || []
|
|
37
|
+
const entries = []
|
|
38
|
+
for (const menu of menus) {
|
|
39
|
+
const rootEntry = menu.url || menu.content ? visit(menu, null) : null
|
|
40
|
+
if (rootEntry) {
|
|
41
|
+
entries.push(rootEntry)
|
|
42
|
+
} else {
|
|
43
|
+
for (const item of menu.items || []) entries.push(visit(item, null))
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// The component version's start page anchors its subtree in Confluence.
|
|
47
|
+
const startPage = url && urlIndex.get(url.split('#')[0])
|
|
48
|
+
const startKey = startPage ? pageKey(startPage.src) : null
|
|
49
|
+
nav.push({ component: component.name, version, displayVersion, title, startKey, entries })
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return { nav, parents, order, navTitles }
|
|
54
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@baiyibai-antora/adf-extension",
|
|
3
|
+
"version": "1.0.0-beta.1",
|
|
4
|
+
"description": "Antora extension that emits Confluence ADF JSON and a publish manifest for every page during a site build",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "baiyibai",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://gitlab.com/baiyibai-antora/confluora.git",
|
|
10
|
+
"directory": "packages/adf-extension"
|
|
11
|
+
},
|
|
12
|
+
"type": "commonjs",
|
|
13
|
+
"main": "lib/index.js",
|
|
14
|
+
"files": [
|
|
15
|
+
"lib"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"test": "node --test test/*-test.js"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@baiyibai-antora/asciidoc-adf-converter": "^1.0.0-beta.1",
|
|
22
|
+
"js-yaml": "^4.1.0"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"antora": "^3.1.10"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"confluora",
|
|
29
|
+
"antora",
|
|
30
|
+
"antora-extension",
|
|
31
|
+
"adf",
|
|
32
|
+
"confluence"
|
|
33
|
+
],
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=18"
|
|
36
|
+
},
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public"
|
|
39
|
+
},
|
|
40
|
+
"homepage": "https://gitlab.com/baiyibai-antora/confluora",
|
|
41
|
+
"bugs": "https://gitlab.com/baiyibai-antora/confluora/-/issues"
|
|
42
|
+
}
|