@antora/assembler 1.0.0-alpha.10 → 1.0.0-alpha.12
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.md +5 -4
- package/lib/assemble-content.js +213 -26
- package/lib/configure.js +96 -0
- package/lib/index.js +2 -1
- package/lib/load-config.js +30 -12
- package/lib/{produce-aggregate-document.js → produce-assembly-file.js} +90 -77
- package/lib/{produce-aggregate-documents.js → produce-assembly-files.js} +61 -66
- package/lib/util/compute-out.js +57 -0
- package/lib/util/create-asciidoc-file.js +18 -0
- package/lib/util/lazy-readable.js +18 -0
- package/package.json +12 -11
package/README.md
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
# Antora Assembler
|
|
2
2
|
|
|
3
|
-
A library for
|
|
3
|
+
A library for merging the AsciiDoc from multiple pages into a single document (i.e., an assembly) and converting that document to another format such as PDF.
|
|
4
4
|
|
|
5
|
-
Assembler works by constructing
|
|
6
|
-
It then
|
|
7
|
-
|
|
5
|
+
Assembler works by constructing AsciiDoc documents from multiple pages using the navigation as a model.
|
|
6
|
+
It then delegates to a converter (typically an external command) provided by an exporter extension to convert those assembly documents to an export format such as PDF.
|
|
7
|
+
Finally, it publishes those exports along with the other files in the site.
|
|
8
|
+
The exports can then be viewed offline, either by the browser or a reader application.
|
|
8
9
|
|
|
9
10
|
[Antora](https://antora.org) is a modular static site generator designed for creating documentation sites from AsciiDoc documents.
|
|
10
11
|
The Assembler extends the feature set of Antora by providing the foundation for page aggregation.
|
package/lib/assemble-content.js
CHANGED
|
@@ -1,46 +1,233 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
+
const computeOut = require('./util/compute-out')
|
|
4
|
+
const fs = require('node:fs')
|
|
5
|
+
const { promises: fsp } = fs
|
|
6
|
+
const LazyReadable = require('./util/lazy-readable')
|
|
3
7
|
const loadConfig = require('./load-config')
|
|
4
|
-
const
|
|
8
|
+
const ospath = require('node:path')
|
|
9
|
+
const { posix: path } = ospath
|
|
10
|
+
const produceAssemblyFiles = require('./produce-assembly-files')
|
|
5
11
|
const PromiseQueue = require('./util/promise-queue')
|
|
6
|
-
const
|
|
12
|
+
const { stringify: toJSON } = JSON
|
|
7
13
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
return this.resolvePage('index.adoc', { component, version })
|
|
13
|
-
}
|
|
14
|
-
}
|
|
14
|
+
const invariably = { new: () => ({}), void: () => undefined }
|
|
15
|
+
const PACKAGE_NAME = require('../package.json').name
|
|
16
|
+
|
|
17
|
+
async function assembleContent (playbook, contentCatalog, converter, { configSource, navigationCatalog }) {
|
|
15
18
|
const assemblerConfig = await loadConfig(playbook, configSource)
|
|
16
19
|
if (!assemblerConfig) return [] // TODO consider removing and doing this another way
|
|
17
|
-
const
|
|
20
|
+
const context = isBound(this)
|
|
21
|
+
? this
|
|
22
|
+
: {
|
|
23
|
+
getFunctions: invariably.new,
|
|
24
|
+
getLogger: invariably.void,
|
|
25
|
+
getVariables: invariably.new,
|
|
26
|
+
}
|
|
27
|
+
const generatorFunctions = context.getFunctions()
|
|
18
28
|
const { loadAsciiDoc = require('@antora/asciidoc-loader') } = generatorFunctions
|
|
19
|
-
const
|
|
20
|
-
|
|
29
|
+
const targetBackend = converter == null ? undefined : (converter.backend ?? converter.extname?.slice(1))
|
|
30
|
+
const { assembly: assemblyConfig, build: buildConfig } = assemblerConfig
|
|
31
|
+
const { profile = targetBackend } = assemblyConfig
|
|
32
|
+
const intrinsicAttributes = { 'loader-assembler': '' }
|
|
33
|
+
if (profile) {
|
|
34
|
+
buildConfig.dir ??= ospath.join(playbook.dir ?? process.cwd(), `build/assembler-${profile}`)
|
|
35
|
+
intrinsicAttributes[`assembler-profile-${profile}`] = ''
|
|
36
|
+
intrinsicAttributes['assembler-profile'] = profile
|
|
37
|
+
if (targetBackend) {
|
|
38
|
+
intrinsicAttributes[`assembler-backend-${targetBackend}`] = ''
|
|
39
|
+
intrinsicAttributes['assembler-backend'] = targetBackend
|
|
40
|
+
}
|
|
41
|
+
} else {
|
|
42
|
+
buildConfig.dir ??= ospath.join(playbook.dir ?? process.cwd(), 'build/assembler')
|
|
43
|
+
}
|
|
44
|
+
Object.assign(assemblerConfig.asciidoc.attributes, intrinsicAttributes)
|
|
45
|
+
const assemblyFiles = produceAssemblyFiles(
|
|
46
|
+
loadAsciiDoc,
|
|
47
|
+
contentCatalog,
|
|
48
|
+
assemblerConfig,
|
|
49
|
+
createResolveAssemblyModel(context, contentCatalog, assemblyConfig, intrinsicAttributes, navigationCatalog)
|
|
50
|
+
)
|
|
51
|
+
const { convert = converter, extname: targetExtname, mediaType: targetMediaType } = converter ?? {}
|
|
52
|
+
if (!(convert && assemblyFiles.length)) return assemblyFiles
|
|
21
53
|
const { publishSite: publishFiles = require('@antora/site-publisher') } = generatorFunctions
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
// TODO: pass more information to convertDocument so it doesn't have to compute internal stuff
|
|
25
|
-
// Q: don't we need to pass in the combined/resolved AsciiDoc attributes per file or component version?
|
|
54
|
+
await prepareWorkspace(publishFiles, assemblyFiles, contentCatalog, buildConfig)
|
|
55
|
+
const boundConvert = convert.bind(context)
|
|
26
56
|
return new PromiseQueue({ concurrency: buildConfig.processLimit })
|
|
27
|
-
.add(
|
|
57
|
+
.add(
|
|
58
|
+
assemblyFiles.map((doc) => async () => {
|
|
59
|
+
const convertAttributes = prepareConvertAttributes(doc, targetExtname, buildConfig)
|
|
60
|
+
if (buildConfig.mkdirs) await fsp.mkdir(convertAttributes.outdir, { recursive: true, force: true })
|
|
61
|
+
return boundConvert(doc, convertAttributes, buildConfig).then(
|
|
62
|
+
(fileOrContents = new LazyReadable(() => fs.createReadStream(convertAttributes.outfile))) => {
|
|
63
|
+
return coerceToExportFormat(doc, targetBackend, targetExtname, targetMediaType, fileOrContents)
|
|
64
|
+
}
|
|
65
|
+
)
|
|
66
|
+
})
|
|
67
|
+
)
|
|
28
68
|
.toPromise()
|
|
29
69
|
.then((files) => {
|
|
30
|
-
if (buildConfig.publish
|
|
31
|
-
|
|
70
|
+
if (!buildConfig.publish) {
|
|
71
|
+
for (const file of files) delete file.assembler.assembled
|
|
72
|
+
return files
|
|
73
|
+
}
|
|
74
|
+
const qualifyExports = buildConfig.qualifyExports
|
|
75
|
+
return files.map((file) => {
|
|
76
|
+
const pages = file.assembler.assembled.pages
|
|
77
|
+
delete file.assembler.assembled
|
|
78
|
+
file = contentCatalog.addFile(Object.assign(file, { out: computeOut.call(contentCatalog, file.src) }))
|
|
79
|
+
const extname = file.extname
|
|
80
|
+
const download = file.assembler.downloadStem + extname
|
|
81
|
+
if (qualifyExports) {
|
|
82
|
+
file.pub.url = '/' + (file.out.path = path.join(file.out.dirname, (file.out.basename = download)))
|
|
83
|
+
} else {
|
|
84
|
+
file.pub.download = download
|
|
85
|
+
}
|
|
86
|
+
pages.forEach((fragment, page) => {
|
|
87
|
+
const assemblerMeta = (page.assembler ??= {})
|
|
88
|
+
;(assemblerMeta.exports ??= []).push({ fragment, file })
|
|
89
|
+
const extnameProp = extname.slice(1)
|
|
90
|
+
if (extnameProp in assemblerMeta) return
|
|
91
|
+
Object.defineProperty(assemblerMeta, extnameProp, {
|
|
92
|
+
configurable: true,
|
|
93
|
+
enumerable: true,
|
|
94
|
+
get: findFirstExportWithExtname.bind(assemblerMeta, extname),
|
|
95
|
+
})
|
|
96
|
+
})
|
|
97
|
+
return file
|
|
98
|
+
})
|
|
32
99
|
})
|
|
33
100
|
}
|
|
34
101
|
|
|
102
|
+
function createResolveAssemblyModel (context, contentCatalog, defaults, intrinsicAttributes, navigationCatalog) {
|
|
103
|
+
const { assemblerProfiles } = context.getVariables()
|
|
104
|
+
const { insertStartPage, rootLevel, sectionMergeStrategy } = defaults
|
|
105
|
+
if (!assemblerProfiles) {
|
|
106
|
+
return (componentVersion) => {
|
|
107
|
+
const navigation =
|
|
108
|
+
navigationCatalog?.getNavigation(componentVersion.name, componentVersion.version) ?? componentVersion.navigation
|
|
109
|
+
return { insertStartPage, rootLevel, sectionMergeStrategy, navigation }
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const boundSendToLog = sendToLog.bind(context.getLogger ? context.getLogger(PACKAGE_NAME) : undefined)
|
|
113
|
+
const { buildNavigation = require('@antora/navigation-builder') } = context.getFunctions()
|
|
114
|
+
return (componentVersion) => {
|
|
115
|
+
const componentVersionProfiles = assemblerProfiles.get(componentVersion.version + '@' + componentVersion.name)
|
|
116
|
+
const { navFiles, messages, ...model } = Object.assign(
|
|
117
|
+
{ insertStartPage, rootLevel, sectionMergeStrategy },
|
|
118
|
+
componentVersionProfiles?.get(intrinsicAttributes['assembler-profile']) ?? componentVersionProfiles?.get()
|
|
119
|
+
)
|
|
120
|
+
const navigationOverride = navigationCatalog?.getNavigation(componentVersion.name, componentVersion.version)
|
|
121
|
+
if (navigationOverride) return Object.assign(model, { navigation: navigationOverride })
|
|
122
|
+
messages?.forEach(boundSendToLog)
|
|
123
|
+
model.navigation = componentVersion.navigation
|
|
124
|
+
if (!navFiles) return model
|
|
125
|
+
if (!navFiles.length) return Object.assign(model, { navigation: [] })
|
|
126
|
+
const navigation_ = model.navigation
|
|
127
|
+
const asciidoc_ = componentVersion.asciidoc
|
|
128
|
+
componentVersion.asciidoc = Object.assign({}, asciidoc_, {
|
|
129
|
+
attributes: Object.assign({}, asciidoc_.attributes, intrinsicAttributes),
|
|
130
|
+
})
|
|
131
|
+
buildNavigation(
|
|
132
|
+
new Proxy(contentCatalog, {
|
|
133
|
+
get (target, property) {
|
|
134
|
+
const method = target[property]
|
|
135
|
+
if (property !== 'findBy') return method
|
|
136
|
+
return (criteria) => (toJSON(criteria) === '{"family":"nav"}' ? navFiles : method.call(target, criteria))
|
|
137
|
+
},
|
|
138
|
+
})
|
|
139
|
+
)
|
|
140
|
+
model.navigation = componentVersion.navigation
|
|
141
|
+
Object.assign(componentVersion, { asciidoc: asciidoc_, navigation: navigation_ })
|
|
142
|
+
return model
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function prepareConvertAttributes (doc, targetExtname, buildConfig) {
|
|
147
|
+
const {
|
|
148
|
+
asciidoc: { attributes: docAttributes } = { attributes: {} },
|
|
149
|
+
extname: docfilesuffix,
|
|
150
|
+
path: reldocfile,
|
|
151
|
+
src: { family, relative },
|
|
152
|
+
} = doc
|
|
153
|
+
const { cwd = process.cwd(), dir = cwd } = buildConfig
|
|
154
|
+
const docname = family + '$' + relative.slice(0, relative.length - docfilesuffix.length)
|
|
155
|
+
const docfile = ospath.join(dir, reldocfile)
|
|
156
|
+
const docdir = ospath.dirname(docfile)
|
|
157
|
+
const imagesdir = '/..'.repeat(reldocfile.split('/').length - 1).slice(1)
|
|
158
|
+
const outfile = docfile.slice(0, docfile.length - docfilesuffix.length) + targetExtname
|
|
159
|
+
const attributes = Object.assign({}, docAttributes, {
|
|
160
|
+
docdir,
|
|
161
|
+
docfile,
|
|
162
|
+
docfilesuffix,
|
|
163
|
+
'docname@': docname,
|
|
164
|
+
imagesdir,
|
|
165
|
+
outdir: docdir,
|
|
166
|
+
outfile,
|
|
167
|
+
outfilesuffix: targetExtname,
|
|
168
|
+
toArgs (optionFlag, command) {
|
|
169
|
+
const padCharRef = process.plaform === 'win32' && command.startsWith('bundle exec ')
|
|
170
|
+
const args = []
|
|
171
|
+
for (let [name, val] of Object.entries(this)) {
|
|
172
|
+
if (val) {
|
|
173
|
+
val = `${name}=${padCharRef && typeof val.charAt === 'function' && val.charAt() === '&' ? ' ' : ''}${val}`
|
|
174
|
+
} else if (val === '') {
|
|
175
|
+
val = name
|
|
176
|
+
} else {
|
|
177
|
+
val = `!${name}${val === false ? '@' : ''}`
|
|
178
|
+
}
|
|
179
|
+
args.push('-a', val)
|
|
180
|
+
}
|
|
181
|
+
return args
|
|
182
|
+
},
|
|
183
|
+
})
|
|
184
|
+
return Object.defineProperty(attributes, 'toArgs', { enumerable: false })
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function coerceToExportFormat (originalFile, targetBackend, targetExtname, targetMediaType, fileOrContents) {
|
|
188
|
+
const file =
|
|
189
|
+
fileOrContents == null || Buffer.isBuffer(fileOrContents) || typeof fileOrContents.pipe === 'function'
|
|
190
|
+
? Object.assign(originalFile, { contents: fileOrContents })
|
|
191
|
+
: fileOrContents
|
|
192
|
+
;(file.assembler ??= {}).backend = targetBackend
|
|
193
|
+
if (file.extname === targetExtname) return file
|
|
194
|
+
const sourcePath = file.path
|
|
195
|
+
const sourceExtname = file.extname
|
|
196
|
+
const relativeWithoutExtname = file.src.relative.slice(0, file.src.relative.length - sourceExtname.length)
|
|
197
|
+
const newPath = sourcePath.slice(0, sourcePath.length - sourceExtname.length) + targetExtname
|
|
198
|
+
Object.assign(file, { mediaType: (file.src.mediaType = targetMediaType), path: newPath })
|
|
199
|
+
file.src.basename = path.basename((file.src.relative = relativeWithoutExtname + (file.src.extname = targetExtname)))
|
|
200
|
+
return file
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function findFirstExportWithExtname (extname) {
|
|
204
|
+
return this.exports.find((it) => it.file.extname === extname)
|
|
205
|
+
}
|
|
206
|
+
|
|
35
207
|
// TODO: if no workspace dir is defined, we shouldn't continue
|
|
36
|
-
function prepareWorkspace (publishFiles,
|
|
37
|
-
const { dir, clean,
|
|
38
|
-
const files =
|
|
39
|
-
|
|
40
|
-
|
|
208
|
+
function prepareWorkspace (publishFiles, assemblyFiles, contentCatalog, buildConfig) {
|
|
209
|
+
const { dir, clean, keepSource } = buildConfig
|
|
210
|
+
const files = []
|
|
211
|
+
const outPaths = new Set()
|
|
212
|
+
for (const file of assemblyFiles) {
|
|
213
|
+
for (const asset of file.assembler.assembled.assets) {
|
|
214
|
+
if (outPaths.has(asset.out.path)) continue
|
|
215
|
+
files.push(asset)
|
|
216
|
+
outPaths.add(asset.out.path)
|
|
217
|
+
}
|
|
41
218
|
}
|
|
42
|
-
|
|
43
|
-
return publishFiles({ output: { clean, dir } },
|
|
219
|
+
if (keepSource) files.push(...assemblyFiles.map((file) => Object.assign(file, { out: { path: file.path } })))
|
|
220
|
+
return publishFiles({ output: { clean, dir } }, { getFiles: () => files })
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function isBound (obj) {
|
|
224
|
+
if (obj == null) return false
|
|
225
|
+
for (const p in obj) return true
|
|
226
|
+
return false
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function sendToLog (levelAndArgs) {
|
|
230
|
+
if (this) this[levelAndArgs[0]].apply(this, levelAndArgs.slice(1))
|
|
44
231
|
}
|
|
45
232
|
|
|
46
233
|
module.exports = assembleContent
|
package/lib/configure.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
function configure (context, ...args) {
|
|
4
|
+
internalConfigure.apply(context, args)
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function internalConfigure (converter, config = {}, providers = {}) {
|
|
8
|
+
this.once('componentsRegistered', ({ contentCatalog, assemblerProfiles }) => {
|
|
9
|
+
this.updateVariables(assemblerProfiles ? undefined : { assemblerProfiles: getAssemblerProfiles(contentCatalog) })
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
this.once('beforeProcess', ({ siteAsciiDocConfig }) => {
|
|
13
|
+
siteAsciiDocConfig.keepSource = true
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
this.once('navigationBuilt', async ({ playbook, contentCatalog }) => {
|
|
17
|
+
const { assembleContent = require('./assemble-content'), ...assembleContentConfig } = providers
|
|
18
|
+
assembleContentConfig.configSource ??= config.configFile
|
|
19
|
+
await assembleContent.call(this, playbook, contentCatalog, converter, assembleContentConfig)
|
|
20
|
+
})
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function getAssemblerProfiles (contentCatalog, assemblerProfiles = new Map()) {
|
|
24
|
+
contentCatalog.getComponents().forEach((component) => {
|
|
25
|
+
component.versions.forEach((componentVersion) => {
|
|
26
|
+
const source = componentVersion.nav?.origin
|
|
27
|
+
const assemblerConfig = getAssemblerConfigFromDescriptor(source?.descriptor)
|
|
28
|
+
if (!assemblerConfig) return
|
|
29
|
+
const componentVersionRef = `${componentVersion.version}@${componentVersion.name}`
|
|
30
|
+
const filesByPath = componentVersion.files.reduce((accum, it) => accum.set(it.path, it), new Map())
|
|
31
|
+
const componentVersionProfiles = assemblerConfig.reduce((accum, entry) => {
|
|
32
|
+
const data = {}
|
|
33
|
+
const profile = entry.profile ?? undefined
|
|
34
|
+
if (accum.has(profile)) return accum
|
|
35
|
+
const nav = entry.nav
|
|
36
|
+
Object.entries(entry).forEach(([key, val]) => {
|
|
37
|
+
if (key === 'profile' || key === 'nav') return
|
|
38
|
+
const camelKey = key.toLowerCase().replace(/[_-]([a-z0-9])/g, (_, l, idx) => (idx ? l.toUpperCase() : l))
|
|
39
|
+
data[camelKey] = val
|
|
40
|
+
})
|
|
41
|
+
if (nav) {
|
|
42
|
+
data.navFiles = (nav.length ? [...new Set(nav)] : nav).reduce((navFiles, path_) => {
|
|
43
|
+
const navFile = filesByPath.get(path_)
|
|
44
|
+
if (navFile) {
|
|
45
|
+
navFiles.push(initNavFile(navFile, component.name, componentVersion.version, navFiles.length))
|
|
46
|
+
} else {
|
|
47
|
+
;(data.messages ??= []).push([
|
|
48
|
+
'warn',
|
|
49
|
+
{ source },
|
|
50
|
+
`Could not resolve nav file for ${profile || '<default>'} profile in ${componentVersionRef}: ${path_}`,
|
|
51
|
+
])
|
|
52
|
+
}
|
|
53
|
+
return navFiles
|
|
54
|
+
}, [])
|
|
55
|
+
} else if (!Object.keys(data).length) {
|
|
56
|
+
return accum
|
|
57
|
+
}
|
|
58
|
+
return accum.set(profile, data)
|
|
59
|
+
}, new Map())
|
|
60
|
+
assemblerProfiles.set(componentVersionRef, componentVersionProfiles)
|
|
61
|
+
})
|
|
62
|
+
})
|
|
63
|
+
return assemblerProfiles
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function getAssemblerConfigFromDescriptor (descriptor = {}) {
|
|
67
|
+
let assemblerConfig = descriptor.ext?.assembler
|
|
68
|
+
if (!assemblerConfig) return
|
|
69
|
+
if (Array.isArray(assemblerConfig)) {
|
|
70
|
+
if (!assemblerConfig.length) return
|
|
71
|
+
} else {
|
|
72
|
+
assemblerConfig = [assemblerConfig]
|
|
73
|
+
}
|
|
74
|
+
return assemblerConfig
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function initNavFile (file, component, version, index) {
|
|
78
|
+
const src = Object.assign({}, file.src, { component, version, module: 'ROOT', family: 'nav' })
|
|
79
|
+
const filePathSegments = file.path.split('/')
|
|
80
|
+
let relativeStartIdx = 0
|
|
81
|
+
if (filePathSegments[0] === 'modules') {
|
|
82
|
+
relativeStartIdx += 1
|
|
83
|
+
if (filePathSegments.length > 2) {
|
|
84
|
+
relativeStartIdx += 1
|
|
85
|
+
src.module = filePathSegments[1]
|
|
86
|
+
if (filePathSegments.length > 3 && filePathSegments[2] === 'partials') {
|
|
87
|
+
relativeStartIdx += 1
|
|
88
|
+
src.family = 'partial'
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
src.relative = filePathSegments.slice(relativeStartIdx).join('/')
|
|
93
|
+
return Object.assign(new file.constructor(file), { nav: { index }, src })
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
module.exports = configure
|
package/lib/index.js
CHANGED
package/lib/load-config.js
CHANGED
|
@@ -29,25 +29,43 @@ function loadConfig (playbook, configSource = './antora-assembler.yml') {
|
|
|
29
29
|
if (!('doctype' in asciidocAttrs)) asciidocAttrs.doctype = 'book'
|
|
30
30
|
if (!('revdate' in asciidocAttrs)) asciidocAttrs.revdate = getLocalDate()
|
|
31
31
|
asciidocAttrs['page-partial'] = null
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
32
|
+
const remapComponentVersionsKey = !('componentVersionFilter' in config)
|
|
33
|
+
const componentVersionFilter = (config.componentVersionFilter ??= {})
|
|
34
|
+
if (remapComponentVersionsKey && 'componentVersions' in config) {
|
|
35
|
+
componentVersionFilter.names = config.componentVersions
|
|
36
|
+
delete config.componentVersions
|
|
37
37
|
}
|
|
38
|
-
if (
|
|
39
|
-
|
|
40
|
-
if (
|
|
41
|
-
|
|
38
|
+
if (componentVersionFilter.names == null) {
|
|
39
|
+
componentVersionFilter.names = ['*']
|
|
40
|
+
} else if (typeof componentVersionFilter.names === 'string') {
|
|
41
|
+
componentVersionFilter.names = componentVersionFilter.names.split(', ')
|
|
42
42
|
}
|
|
43
|
-
const
|
|
43
|
+
const remapAssemblyKeys = !('assembly' in config)
|
|
44
|
+
const assembly = (config.assembly ??= {})
|
|
45
|
+
if (remapAssemblyKeys) {
|
|
46
|
+
for (const key of ['rootLevel', 'insertStartPage', 'sectionMergeStrategy']) {
|
|
47
|
+
if (!(key in config)) continue
|
|
48
|
+
assembly[key] = config[key]
|
|
49
|
+
delete config[key]
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (!('rootLevel' in assembly)) assembly.rootLevel = 0
|
|
53
|
+
if (!('insertStartPage' in assembly)) assembly.insertStartPage = true
|
|
54
|
+
if (['discrete', 'fuse', 'enclose'].indexOf(assembly.sectionMergeStrategy) < 0) {
|
|
55
|
+
assembly.sectionMergeStrategy = 'discrete'
|
|
56
|
+
}
|
|
57
|
+
const build = (config.build ??= {})
|
|
44
58
|
if (build.dir === '$' + '{playbook.output.dir}') {
|
|
45
59
|
throw new Error('Not implemented')
|
|
46
60
|
}
|
|
47
|
-
build.dir
|
|
48
|
-
build.cwd = playbook.dir // use playbook.dir for
|
|
61
|
+
build.dir &&= expandPath(build.dir, { dot: playbook.dir })
|
|
62
|
+
build.cwd = playbook.dir // use playbook.dir for finding and loading require scripts
|
|
49
63
|
if (!('clean' in build) && 'output' in playbook) build.clean = playbook.output.clean
|
|
50
64
|
if (!('publish' in build)) build.publish = true
|
|
65
|
+
if ('keepAggregateSource' in build) {
|
|
66
|
+
build.keepSource = build.keepAggregateSource
|
|
67
|
+
delete build.keepAggregateSource
|
|
68
|
+
}
|
|
51
69
|
if (!build.processLimit) {
|
|
52
70
|
build.processLimit = 'processLimit' in build ? Infinity : Math.round(os.cpus().length * 0.5)
|
|
53
71
|
}
|
|
@@ -1,58 +1,55 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
+
const createAsciiDocFile = require('./util/create-asciidoc-file')
|
|
3
4
|
const path = require('node:path/posix')
|
|
4
5
|
const sanitize = require('./util/sanitize')
|
|
5
6
|
const unconvertInlineAsciiDoc = require('./util/unconvert-inline-asciidoc')
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
const BuiltInNamedEntities = { amp: '&', apos: "'", gt: '>', lt: '<', nbsp: ' ', quot: '"' }
|
|
9
|
+
const CharRefRx = /&(?:([a-z][a-z]+\d{0,2})|#(?:(\d{2,6})|x([a-z\d]{2,5})));/g
|
|
10
|
+
|
|
11
|
+
function produceAssemblyFile (
|
|
8
12
|
loadAsciiDoc,
|
|
9
13
|
contentCatalog,
|
|
10
14
|
componentVersion,
|
|
11
15
|
outline,
|
|
16
|
+
files,
|
|
12
17
|
doctype,
|
|
13
|
-
pages,
|
|
14
18
|
asciidocConfig,
|
|
15
19
|
mutableAttributes,
|
|
16
20
|
sectionMergeStrategy = 'discrete'
|
|
17
21
|
) {
|
|
18
|
-
const
|
|
22
|
+
const pagesByUrl = files.reduce((map, it) => (it.src.family === 'page' ? map.set(it.pub.url, it) : map), new Map())
|
|
23
|
+
if (outline.urlType === 'internal' && !pagesByUrl.get(outline.url) && !(outline.items || []).length) return
|
|
24
|
+
const pagesInOutline = selectPagesInOutline(outline, pagesByUrl)
|
|
19
25
|
const navtitle = outline.content
|
|
20
|
-
const
|
|
21
|
-
src: {
|
|
22
|
-
component: componentVersion.name,
|
|
23
|
-
version: componentVersion.version,
|
|
24
|
-
module: 'ROOT',
|
|
25
|
-
family: 'page',
|
|
26
|
-
relative: generateSlug(navtitle),
|
|
27
|
-
},
|
|
28
|
-
})
|
|
29
|
-
const { dir: outDir, name: outName } = path.parse(templateFile.out.path)
|
|
30
|
-
const path_ = outName === templateFile.src.relative ? path.join(outDir, outName + '.adoc') : outDir + '.adoc'
|
|
31
|
-
contentCatalog.removeFile(templateFile)
|
|
32
|
-
const header = buildAsciiDocHeader(componentVersion, navtitle, doctype)
|
|
33
|
-
const body = aggregateAsciiDoc(
|
|
26
|
+
const buffer = mergeAsciiDoc(
|
|
34
27
|
loadAsciiDoc,
|
|
35
28
|
contentCatalog,
|
|
36
|
-
|
|
29
|
+
buildAsciiDocHeader(componentVersion, navtitle, doctype),
|
|
37
30
|
componentVersion,
|
|
38
31
|
outline,
|
|
32
|
+
files,
|
|
39
33
|
pagesInOutline,
|
|
40
34
|
asciidocConfig,
|
|
41
35
|
mutableAttributes,
|
|
42
36
|
sectionMergeStrategy
|
|
43
37
|
)
|
|
44
|
-
|
|
45
|
-
|
|
38
|
+
const level = outline.index ? 0 : 1
|
|
39
|
+
const stem = level === 0 ? 'index' : generateSlug(navtitle)
|
|
40
|
+
const downloadStem = [componentVersion.name, componentVersion.version, level === 0 ? '' : stem]
|
|
41
|
+
.filter((it) => it)
|
|
42
|
+
.join('-')
|
|
43
|
+
return createAsciiDocFile(contentCatalog, {
|
|
46
44
|
asciidoc: asciidocConfig,
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
path: path_,
|
|
45
|
+
assembler: { assembled: pagesInOutline.assembled, downloadStem, level },
|
|
46
|
+
contents: Buffer.from(buffer.join('\n') + '\n'),
|
|
50
47
|
src: {
|
|
51
48
|
component: componentVersion.name,
|
|
52
49
|
version: componentVersion.version,
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
50
|
+
module: 'ROOT',
|
|
51
|
+
family: 'export',
|
|
52
|
+
relative: stem + '.adoc',
|
|
56
53
|
},
|
|
57
54
|
})
|
|
58
55
|
}
|
|
@@ -77,25 +74,25 @@ function buildAsciiDocHeader (componentVersion, navtitle, doctype = 'book') {
|
|
|
77
74
|
]
|
|
78
75
|
}
|
|
79
76
|
|
|
80
|
-
function selectPagesInOutline (outlineEntry,
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
page
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
)
|
|
77
|
+
function selectPagesInOutline (outlineEntry, pagesByUrl, accum) {
|
|
78
|
+
accum ??= Object.assign(new Map(), { assembled: { pages: new Map(), assets: new Set() } })
|
|
79
|
+
const page = outlineEntry.urlType === 'internal' ? pagesByUrl.get(outlineEntry.url) : undefined
|
|
80
|
+
if (page) {
|
|
81
|
+
accum
|
|
82
|
+
.set(`${page.src.module === 'ROOT' ? '' : page.src.module + ':'}${page.src.relative}`, page)
|
|
83
|
+
.set(page.pub.url, page)
|
|
84
|
+
}
|
|
85
|
+
for (const item of outlineEntry.items || []) selectPagesInOutline(item, pagesByUrl, accum)
|
|
86
|
+
return accum
|
|
91
87
|
}
|
|
92
88
|
|
|
93
|
-
function
|
|
89
|
+
function mergeAsciiDoc (
|
|
94
90
|
loadAsciiDoc,
|
|
95
91
|
contentCatalog,
|
|
96
|
-
|
|
92
|
+
buffer,
|
|
97
93
|
componentVersion,
|
|
98
94
|
outlineEntry,
|
|
95
|
+
files,
|
|
99
96
|
pagesInOutline,
|
|
100
97
|
asciidocConfig,
|
|
101
98
|
mutableAttributes,
|
|
@@ -103,7 +100,6 @@ function aggregateAsciiDoc (
|
|
|
103
100
|
lastComponentVersion = componentVersion,
|
|
104
101
|
level = 0
|
|
105
102
|
) {
|
|
106
|
-
const buffer = []
|
|
107
103
|
// TODO: we could try to be smart about it and make sure the page with fragment is included at least once
|
|
108
104
|
if (outlineEntry.hash) return buffer
|
|
109
105
|
const { content: navtitle, items = [], unresolved, urlType, url } = outlineEntry
|
|
@@ -116,7 +112,7 @@ function aggregateAsciiDoc (
|
|
|
116
112
|
})(asciidocConfig.attributes['site-url'])
|
|
117
113
|
// FIXME: ideally, resource ID would be stored in navigation so we can look up the page more efficiently
|
|
118
114
|
let page = urlType === 'internal' && !unresolved ? pagesInOutline.get(url) : undefined
|
|
119
|
-
if (page && pagesInOutline.
|
|
115
|
+
if (page && pagesInOutline.assembled.pages.has(page)) page = undefined
|
|
120
116
|
if (page) {
|
|
121
117
|
let contents = page.src.contents
|
|
122
118
|
if (contents == null) return buffer
|
|
@@ -129,21 +125,19 @@ function aggregateAsciiDoc (
|
|
|
129
125
|
.replace(/^(?:[ \t]*\r\n?|[ \t]*\n)+/, '')
|
|
130
126
|
.trimRight()
|
|
131
127
|
)
|
|
132
|
-
;(pagesInOutline.aggregated ??= []).push(page)
|
|
133
|
-
page = new page.constructor(Object.assign({}, page, { contents, mediaType: 'text/asciidoc' }))
|
|
134
128
|
const { component, version, module: module_, relative, origin } = page.src
|
|
135
129
|
const topicPrefix = ~relative.indexOf('/') ? path.dirname(relative) + '/' : ''
|
|
136
|
-
const
|
|
130
|
+
const pageAsAsciiDoc = new page.constructor(Object.assign({}, page, { contents, mediaType: page.src.mediaType }))
|
|
131
|
+
const doc = loadAsciiDoc(pageAsAsciiDoc, contentCatalog, asciidocConfig)
|
|
137
132
|
const refs = doc.getCatalog().refs
|
|
138
133
|
// NOTE: in Antora, docname is relative src path from module without file extension
|
|
139
134
|
const docname = doc.getAttribute('docname')
|
|
140
135
|
const docnameForId = docname.replace(/[/]/g, '::').replace(/[.]/g, '-')
|
|
141
|
-
const
|
|
142
|
-
const
|
|
143
|
-
(
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
':::'
|
|
136
|
+
const qualifyId = component !== componentVersion.name
|
|
137
|
+
const idscope =
|
|
138
|
+
(qualifyId ? component + ':' : '') + (module_ === 'ROOT' ? (qualifyId ? ':' : '') : module_ + ':') + docnameForId
|
|
139
|
+
const idprefix = idscope + ':::'
|
|
140
|
+
let pageFragment = ''
|
|
147
141
|
buffer.push('')
|
|
148
142
|
buffer.push(`:docname: ${docname}`)
|
|
149
143
|
if (component !== lastComponentVersion.name) {
|
|
@@ -174,17 +168,18 @@ function aggregateAsciiDoc (
|
|
|
174
168
|
if (level === 1 && navtitlePlain === componentVersion.title) {
|
|
175
169
|
level--
|
|
176
170
|
} else {
|
|
171
|
+
pageFragment = `#${idscope}`
|
|
177
172
|
let hlevel = level + 1
|
|
178
173
|
if (hlevel > 6) {
|
|
179
174
|
hlevel = 6
|
|
180
|
-
buffer.push(`[discrete
|
|
175
|
+
buffer.push(`[discrete${pageFragment}]`)
|
|
181
176
|
} else {
|
|
182
|
-
buffer.push(`[
|
|
177
|
+
buffer.push(`[${pageFragment}]`)
|
|
183
178
|
}
|
|
184
179
|
buffer.push(`${'='.repeat(hlevel)} ${navtitleAsciiDoc}`)
|
|
185
180
|
}
|
|
186
181
|
} else {
|
|
187
|
-
|
|
182
|
+
buffer.unshift(`[#${idscope}]`)
|
|
188
183
|
}
|
|
189
184
|
if (sectionMergeStrategy === 'enclose' && hasItems && doc.hasSections()) {
|
|
190
185
|
enclosed = true
|
|
@@ -213,6 +208,7 @@ function aggregateAsciiDoc (
|
|
|
213
208
|
buffer.push(`${'='.repeat(hlevel)} ${overviewTitle}`)
|
|
214
209
|
if (toggleSectids) buffer.push(':sectids:')
|
|
215
210
|
}
|
|
211
|
+
pagesInOutline.assembled.pages.set(page, pageFragment)
|
|
216
212
|
const lines = doc.getSourceLines()
|
|
217
213
|
const ignoreLines = []
|
|
218
214
|
// TODO: think more about when multipart is allowed; perhaps configurable
|
|
@@ -335,7 +331,7 @@ function aggregateAsciiDoc (
|
|
|
335
331
|
.replace(/[/]/g, '::')
|
|
336
332
|
.replace(/\.adoc$/, '')
|
|
337
333
|
.replace(/[.]/g, '-')
|
|
338
|
-
const refid = `${pagePart}:::${fragment}`
|
|
334
|
+
const refid = fragment ? `${pagePart}:::${fragment}` : pagePart
|
|
339
335
|
return `<<${refid}${text && text !== targetPage.title ? ',' + text.replace(/\\]/g, ']') : ''}>>`
|
|
340
336
|
})
|
|
341
337
|
}
|
|
@@ -360,7 +356,7 @@ function aggregateAsciiDoc (
|
|
|
360
356
|
const image = contentCatalog.resolveResource(target, page.src, 'image', ['image'])
|
|
361
357
|
// TODO: handle (or report) unresolved image better
|
|
362
358
|
if (image?.out) {
|
|
363
|
-
|
|
359
|
+
pagesInOutline.assembled.assets.add(image)
|
|
364
360
|
return `image:${image.out.path.replace(/_/g, '{underscore}')}[${attrlist}]`
|
|
365
361
|
}
|
|
366
362
|
}
|
|
@@ -420,7 +416,7 @@ function aggregateAsciiDoc (
|
|
|
420
416
|
// FIXME: handle (or report) case when image is not resolved
|
|
421
417
|
if (image?.out) {
|
|
422
418
|
const boxedAttrlist = line.slice(line.indexOf('['))
|
|
423
|
-
|
|
419
|
+
pagesInOutline.assembled.assets.add(image)
|
|
424
420
|
lines[idx] = `${prefix}image::${image.out.path}${boxedAttrlist}`
|
|
425
421
|
}
|
|
426
422
|
}
|
|
@@ -433,7 +429,10 @@ function aggregateAsciiDoc (
|
|
|
433
429
|
if (block.getId()) rewriteStyleAttribute(block, lines, idx, idprefix)
|
|
434
430
|
}
|
|
435
431
|
})
|
|
436
|
-
|
|
432
|
+
safePush(
|
|
433
|
+
buffer,
|
|
434
|
+
lines.filter((it) => it !== undefined)
|
|
435
|
+
)
|
|
437
436
|
const attributeEntries = Object.entries(doc.source_header_attributes?.$$smap || {})
|
|
438
437
|
if (attributeEntries.length) {
|
|
439
438
|
const resolvedAttributeEntries = attributeEntries.reduce(
|
|
@@ -461,7 +460,7 @@ function aggregateAsciiDoc (
|
|
|
461
460
|
},
|
|
462
461
|
['']
|
|
463
462
|
)
|
|
464
|
-
if (resolvedAttributeEntries.length > 1) buffer
|
|
463
|
+
if (resolvedAttributeEntries.length > 1) safePush(buffer, resolvedAttributeEntries)
|
|
465
464
|
}
|
|
466
465
|
} else if (level) {
|
|
467
466
|
if (level === 1 && navtitlePlain === componentVersion.title) {
|
|
@@ -486,7 +485,7 @@ function aggregateAsciiDoc (
|
|
|
486
485
|
if (urlType === 'external') {
|
|
487
486
|
sectionTitle = `${url}[${navtitleAsciiDoc.replace(/\]/g, '\\]')}]`
|
|
488
487
|
} else if (urlType === 'internal' && !unresolved && siteUrl) {
|
|
489
|
-
const resource =
|
|
488
|
+
const resource = files.find((it) => it.pub.url === url)
|
|
490
489
|
if (resource) sectionTitle = `${siteUrl}${resource.pub.url}[${navtitleAsciiDoc.replace(/\]/g, '\\]')}]`
|
|
491
490
|
}
|
|
492
491
|
let hlevel = level + 1
|
|
@@ -508,20 +507,19 @@ function aggregateAsciiDoc (
|
|
|
508
507
|
? items.slice(1)
|
|
509
508
|
: items
|
|
510
509
|
).forEach((item) => {
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
)
|
|
510
|
+
mergeAsciiDoc(
|
|
511
|
+
loadAsciiDoc,
|
|
512
|
+
contentCatalog,
|
|
513
|
+
buffer,
|
|
514
|
+
componentVersion,
|
|
515
|
+
item,
|
|
516
|
+
files,
|
|
517
|
+
pagesInOutline,
|
|
518
|
+
asciidocConfig,
|
|
519
|
+
mutableAttributes,
|
|
520
|
+
sectionMergeStrategy,
|
|
521
|
+
lastComponentVersion,
|
|
522
|
+
nextLevel
|
|
525
523
|
)
|
|
526
524
|
})
|
|
527
525
|
}
|
|
@@ -531,9 +529,14 @@ function aggregateAsciiDoc (
|
|
|
531
529
|
function generateSlug (title) {
|
|
532
530
|
return title
|
|
533
531
|
.toLowerCase()
|
|
534
|
-
.replace(
|
|
535
|
-
.replace(
|
|
536
|
-
|
|
532
|
+
.replace(/<[^>]+>/g, '')
|
|
533
|
+
.replace(CharRefRx, (_, name, dec, hex) => {
|
|
534
|
+
if (name) return BuiltInNamedEntities[name] ?? '?'
|
|
535
|
+
return String.fromCharCode(dec ? parseInt(dec, 10) : parseInt(hex, 16))
|
|
536
|
+
})
|
|
537
|
+
.replace(/[\x27\u2019]/g, '')
|
|
538
|
+
.replace(/[^\p{Alpha}0-9\-]/gu, '-')
|
|
539
|
+
.replace(/^-+|-+$|(-)-+/g, '$1')
|
|
537
540
|
}
|
|
538
541
|
|
|
539
542
|
function fixSectionLevels (sections, multipart) {
|
|
@@ -602,4 +605,14 @@ function getObjectId (obj) {
|
|
|
602
605
|
return global.Opal.uid()
|
|
603
606
|
}
|
|
604
607
|
|
|
605
|
-
|
|
608
|
+
function safePush (onto, entries) {
|
|
609
|
+
try {
|
|
610
|
+
onto.push(...entries)
|
|
611
|
+
} catch (err) {
|
|
612
|
+
/* istanbul ignore if */
|
|
613
|
+
if (!(err instanceof RangeError)) throw err
|
|
614
|
+
for (const e of entries) onto.push(e)
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
module.exports = produceAssemblyFile
|
|
@@ -1,22 +1,31 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
+
const createAsciiDocFile = require('./util/create-asciidoc-file')
|
|
3
4
|
const filterComponentVersions = require('./filter-component-versions')
|
|
4
|
-
const
|
|
5
|
+
const produceAssemblyFile = require('./produce-assembly-file')
|
|
5
6
|
const selectMutableAttributes = require('./select-mutable-attributes')
|
|
6
7
|
|
|
7
8
|
const IMAGE_MACRO_RX = /^image::?(.+?)\[(.*?)\]$/
|
|
8
9
|
|
|
9
|
-
function
|
|
10
|
-
const {
|
|
10
|
+
function produceAssemblyFiles (loadAsciiDoc, contentCatalog, assemblerConfig, resolveAssemblyModel) {
|
|
11
|
+
const { asciidoc: assemblerAsciiDocConfig, assembly: assemblyConfig } = assemblerConfig
|
|
12
|
+
resolveAssemblyModel ??= (componentVersion) => ({
|
|
13
|
+
insertStartPage: assemblyConfig.insertStartPage,
|
|
14
|
+
rootLevel: assemblyConfig.rootLevel,
|
|
15
|
+
sectionMergeStrategy: assemblyConfig.sectionMergeStrategy,
|
|
16
|
+
navigation: componentVersion.navigation,
|
|
17
|
+
})
|
|
11
18
|
const assemblerAsciiDocAttributes = Object.assign({}, assemblerAsciiDocConfig.attributes)
|
|
12
19
|
const { doctype, revdate, 'source-highlighter': sourceHighlighter } = assemblerAsciiDocAttributes
|
|
13
20
|
delete assemblerAsciiDocAttributes.doctype
|
|
14
21
|
delete assemblerAsciiDocAttributes.revdate
|
|
15
22
|
delete assemblerAsciiDocAttributes['source-highlighter']
|
|
16
|
-
|
|
23
|
+
const publishableFiles = contentCatalog.getFiles().filter((file) => file.out)
|
|
24
|
+
return filterComponentVersions(contentCatalog.getComponents(), assemblerConfig.componentVersionFilter.names).reduce(
|
|
17
25
|
(accum, componentVersion) => {
|
|
18
|
-
const {
|
|
26
|
+
const { insertStartPage, rootLevel, sectionMergeStrategy, navigation } = resolveAssemblyModel(componentVersion)
|
|
19
27
|
if (!navigation) return accum
|
|
28
|
+
const { name: componentName, version, title } = componentVersion
|
|
20
29
|
const componentVersionAsciiDocConfig = getAsciiDocConfigWithAsciidoctorReducerExtension(componentVersion)
|
|
21
30
|
const mergedAsciiDocConfig = Object.assign({}, componentVersionAsciiDocConfig, {
|
|
22
31
|
attributes: Object.assign({ revdate }, componentVersionAsciiDocConfig.attributes, assemblerAsciiDocAttributes),
|
|
@@ -32,37 +41,44 @@ function produceAggregateDocuments (loadAsciiDoc, contentCatalog, assemblerConfi
|
|
|
32
41
|
image.out.assembled = true
|
|
33
42
|
})
|
|
34
43
|
const rootEntry = { content: title }
|
|
35
|
-
let startPage =
|
|
44
|
+
let startPage =
|
|
45
|
+
'startPage' in componentVersion
|
|
46
|
+
? componentVersion.startPage
|
|
47
|
+
: contentCatalog.resolvePage('index.adoc', { component: componentName, version })
|
|
36
48
|
if (startPage && startPage.src.component === componentName && startPage.src.version === version) {
|
|
37
|
-
if (insertStartPage
|
|
38
|
-
|
|
49
|
+
if (insertStartPage) {
|
|
50
|
+
const navtitle = startPage.asciidoc?.navtitle || rootEntry.content
|
|
51
|
+
Object.assign(rootEntry, { navtitle, url: startPage.pub.url, urlType: 'internal' })
|
|
39
52
|
}
|
|
40
53
|
} else {
|
|
41
54
|
// Q: should we always use a reference page as startPage for computing mutableAttributes?
|
|
42
|
-
startPage =
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
55
|
+
startPage = createAsciiDocFile(contentCatalog, {
|
|
56
|
+
src: {
|
|
57
|
+
component: componentVersion.name,
|
|
58
|
+
version: componentVersion.version,
|
|
59
|
+
module: 'ROOT',
|
|
60
|
+
family: 'page',
|
|
61
|
+
relative: '.start-page.adoc',
|
|
62
|
+
origin: (componentVersion.origins || [])[0],
|
|
63
|
+
},
|
|
47
64
|
})
|
|
48
65
|
}
|
|
49
66
|
const mutableAttributes = selectMutableAttributes(loadAsciiDoc, contentCatalog, startPage, mergedAsciiDocConfig)
|
|
50
67
|
delete mutableAttributes.doctype
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
sectionMergeStrategy
|
|
63
|
-
)
|
|
68
|
+
prepareOutlines(navigation, rootEntry, rootLevel).forEach((outline) => {
|
|
69
|
+
const assemblyFile = produceAssemblyFile(
|
|
70
|
+
loadAsciiDoc,
|
|
71
|
+
contentCatalog,
|
|
72
|
+
componentVersion,
|
|
73
|
+
outline,
|
|
74
|
+
publishableFiles,
|
|
75
|
+
doctype,
|
|
76
|
+
mergedAsciiDocConfig,
|
|
77
|
+
mutableAttributes,
|
|
78
|
+
sectionMergeStrategy
|
|
64
79
|
)
|
|
65
|
-
|
|
80
|
+
if (assemblyFile) accum.push(assemblyFile)
|
|
81
|
+
})
|
|
66
82
|
mergedAsciiDocAttributes.doctype = doctype
|
|
67
83
|
sourceHighlighter
|
|
68
84
|
? (mergedAsciiDocAttributes['source-highlighter'] = sourceHighlighter)
|
|
@@ -73,33 +89,6 @@ function produceAggregateDocuments (loadAsciiDoc, contentCatalog, assemblerConfi
|
|
|
73
89
|
)
|
|
74
90
|
}
|
|
75
91
|
|
|
76
|
-
function createFile (src) {
|
|
77
|
-
const familySegment = (src.family ??= 'page') + 's'
|
|
78
|
-
const relativeSegments = src.relative.split('/')
|
|
79
|
-
const segments = ['modules', (src.module ??= 'ROOT'), familySegment, ...relativeSegments]
|
|
80
|
-
const path = segments.join('/')
|
|
81
|
-
const moduleRootPath = Array(relativeSegments.length - 1)
|
|
82
|
-
.fill('..')
|
|
83
|
-
.join('/')
|
|
84
|
-
const outPath = [
|
|
85
|
-
src.component === 'ROOT' ? '' : src.component,
|
|
86
|
-
src.version,
|
|
87
|
-
src.module === 'ROOT' ? '' : src.module,
|
|
88
|
-
src.family === 'page' ? '' : '_' + familySegment,
|
|
89
|
-
src.family === 'page' ? src.relative.replace(/\.adoc$/, '.html') : src.relative,
|
|
90
|
-
]
|
|
91
|
-
.filter((it) => it)
|
|
92
|
-
.join('/')
|
|
93
|
-
return {
|
|
94
|
-
path,
|
|
95
|
-
dirname: path.slice(0, path.lastIndexOf('/')),
|
|
96
|
-
contents: src.contents ?? Buffer.alloc(0),
|
|
97
|
-
src,
|
|
98
|
-
out: { path: outPath },
|
|
99
|
-
pub: { url: '/' + outPath, moduleRootPath },
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
92
|
function getAsciiDocConfigWithAsciidoctorReducerExtension (componentVersion) {
|
|
104
93
|
const asciidoctorReducerExtension = require('@asciidoctor/reducer') // NOTE: must be required lazily
|
|
105
94
|
const asciidocConfig = componentVersion.asciidoc
|
|
@@ -123,21 +112,27 @@ function isResourceRef (target) {
|
|
|
123
112
|
return ~target.indexOf(':') && !(~target.indexOf('://') || (target.startsWith('data:') && ~target.indexOf(',')))
|
|
124
113
|
}
|
|
125
114
|
|
|
126
|
-
// when root level is 0, merge the navigation into the rootEntry
|
|
127
|
-
// when root level is 1, create navigation per navigation menu
|
|
128
|
-
// in this case, if there's only a single navigation menu with no title, promote each top-level item to a menu
|
|
129
115
|
function prepareOutlines (navigation, rootEntry, rootLevel) {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
if (
|
|
133
|
-
|
|
116
|
+
const singleEntry = navigation.length === 1
|
|
117
|
+
const items = navigation.reduce((accum, it) => {
|
|
118
|
+
if (!it.content || (singleEntry && it.url ? it.url === rootEntry.url : it.content === rootEntry.content)) {
|
|
119
|
+
accum.push(...it.items)
|
|
134
120
|
} else {
|
|
135
|
-
|
|
136
|
-
|
|
121
|
+
accum.push(it)
|
|
122
|
+
}
|
|
123
|
+
return accum
|
|
124
|
+
}, [])
|
|
125
|
+
if (rootLevel === 0) {
|
|
126
|
+
if (rootEntry.url && includedInNav(items, rootEntry.url)) {
|
|
127
|
+
for (const p of ['url', 'urlType']) delete rootEntry[p]
|
|
137
128
|
}
|
|
138
|
-
return
|
|
129
|
+
return [Object.assign(rootEntry, { index: true, items })]
|
|
130
|
+
}
|
|
131
|
+
if (rootEntry.url && !includedInNav(items, rootEntry.url)) {
|
|
132
|
+
rootEntry.content = singleEntry && rootEntry.url === navigation[0].url ? navigation[0].content : rootEntry.navtitle
|
|
133
|
+
items.unshift(rootEntry)
|
|
139
134
|
}
|
|
140
|
-
return
|
|
135
|
+
return items
|
|
141
136
|
}
|
|
142
137
|
|
|
143
|
-
module.exports =
|
|
138
|
+
module.exports = produceAssemblyFiles
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { posix: path } = require('node:path')
|
|
4
|
+
|
|
5
|
+
function computeOut (src) {
|
|
6
|
+
const { component, version, module: module_, family, relative } = src
|
|
7
|
+
const outRelative = family === 'page' ? relative.replace(/\.adoc$/, '.html') : relative
|
|
8
|
+
const { dir: dirname, base: basename, ext: extname, name: stem } = path.parse(outRelative)
|
|
9
|
+
const componentVersion = this.getComponentVersion(component, version)
|
|
10
|
+
const versionSegment =
|
|
11
|
+
'activeVersionSegment' in componentVersion
|
|
12
|
+
? componentVersion.activeVersionSegment
|
|
13
|
+
: resolveActiveVersionSegment.call(this, component, version)
|
|
14
|
+
const outDirSegments = []
|
|
15
|
+
const moduleRootPathSegments = []
|
|
16
|
+
if (component !== 'ROOT') outDirSegments.push(component)
|
|
17
|
+
if (versionSegment) outDirSegments.push(versionSegment)
|
|
18
|
+
if (module_ !== 'ROOT') outDirSegments.push(module_)
|
|
19
|
+
const outModuleDirSegments = outDirSegments.slice()
|
|
20
|
+
if (family !== 'page') {
|
|
21
|
+
outDirSegments.push(`_${family}s`)
|
|
22
|
+
moduleRootPathSegments.push('..')
|
|
23
|
+
}
|
|
24
|
+
if (dirname) {
|
|
25
|
+
outDirSegments.push(dirname)
|
|
26
|
+
for (const _ of dirname.split('/')) moduleRootPathSegments.push('..')
|
|
27
|
+
}
|
|
28
|
+
const rootPathSegments = moduleRootPathSegments.slice()
|
|
29
|
+
for (const _ of outModuleDirSegments) rootPathSegments.push('..')
|
|
30
|
+
const outDirname = outDirSegments.join('/')
|
|
31
|
+
const result = {
|
|
32
|
+
dirname: outDirname,
|
|
33
|
+
basename,
|
|
34
|
+
path: outDirname + '/' + basename,
|
|
35
|
+
moduleRootPath: moduleRootPathSegments.length ? moduleRootPathSegments.join('/') : '.',
|
|
36
|
+
rootPath: rootPathSegments.length ? rootPathSegments.join('/') : '.',
|
|
37
|
+
}
|
|
38
|
+
return result
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function resolveActiveVersionSegment (component, version) {
|
|
42
|
+
let startPage = this.resolvePage('index.adoc', { component, version })
|
|
43
|
+
let startPageSrc
|
|
44
|
+
if (startPage) {
|
|
45
|
+
startPageSrc = startPage.src
|
|
46
|
+
} else {
|
|
47
|
+
startPageSrc = { component, version, module: 'ROOT', family: 'page', relative: 'index.adoc' }
|
|
48
|
+
this.removeFile((startPage = this.addFile({ src: startPageSrc })))
|
|
49
|
+
}
|
|
50
|
+
const outPathSegments = startPage.out.path.split('/')
|
|
51
|
+
for (const depth of startPage.out.moduleRootPath.split('/')) outPathSegments.pop()
|
|
52
|
+
if (startPageSrc.module !== 'ROOT') outPathSegments.pop()
|
|
53
|
+
if (startPageSrc.component !== 'ROOT') outPathSegments.shift()
|
|
54
|
+
return outPathSegments.length ? outPathSegments[0] : ''
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
module.exports = computeOut
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const computeOut = require('./compute-out')
|
|
4
|
+
const { posix: path } = require('node:path')
|
|
5
|
+
|
|
6
|
+
function createAsciiDocFile (contentCatalog, file) {
|
|
7
|
+
file.mediaType = 'text/asciidoc'
|
|
8
|
+
const src = file.src
|
|
9
|
+
const out = computeOut.call(contentCatalog, src)
|
|
10
|
+
if (src.family === 'export') {
|
|
11
|
+
contentCatalog.removeFile((file = contentCatalog.addFile(Object.assign(file, { path: out.path, out: null }))))
|
|
12
|
+
return file
|
|
13
|
+
}
|
|
14
|
+
const pub = { url: '/' + out.path, moduleRootPath: out.moduleRootPath, rootPath: out.rootPath }
|
|
15
|
+
return { contents: src.contents ?? Buffer.alloc(0), src, out, pub }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
module.exports = createAsciiDocFile
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { PassThrough } = require('node:stream')
|
|
4
|
+
|
|
5
|
+
// adapted from https://github.com/jpommerening/node-lazystream/blob/master/lib/lazystream.js | license: MIT
|
|
6
|
+
class LazyReadable extends PassThrough {
|
|
7
|
+
constructor (fn, options) {
|
|
8
|
+
super(options)
|
|
9
|
+
this._read = function () {
|
|
10
|
+
delete this._read // restores original method
|
|
11
|
+
fn.call(this, options).on('error', this.emit.bind(this, 'error')).pipe(this)
|
|
12
|
+
return this._read.apply(this, arguments)
|
|
13
|
+
}
|
|
14
|
+
this.emit('readable')
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
module.exports = LazyReadable
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@antora/assembler",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.12",
|
|
4
4
|
"description": "An extension library for Antora that assembles content from multiple pages into a single AsciiDoc file to converted and publish.",
|
|
5
5
|
"license": "MPL-2.0",
|
|
6
6
|
"author": "OpenDevise Inc. (https://opendevise.com)",
|
|
@@ -9,7 +9,11 @@
|
|
|
9
9
|
"Sarah White <sarah@opendevise.com>"
|
|
10
10
|
],
|
|
11
11
|
"homepage": "https://antora.org",
|
|
12
|
-
"repository":
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://gitlab.com/antora/antora-assembler.git",
|
|
15
|
+
"directory": "packages/assembler"
|
|
16
|
+
},
|
|
13
17
|
"bugs": {
|
|
14
18
|
"url": "https://gitlab.com/antora/antora-assembler/issues"
|
|
15
19
|
},
|
|
@@ -21,26 +25,26 @@
|
|
|
21
25
|
"main": "lib/index.js",
|
|
22
26
|
"exports": {
|
|
23
27
|
".": "./lib/index.js",
|
|
24
|
-
"./asciidoctor/reducer-extension": "./lib/asciidoctor/reducer-extension.js",
|
|
25
28
|
"./filter-component-versions": "./lib/filter-component-versions.js",
|
|
26
29
|
"./load-config": "./lib/load-config.js",
|
|
27
|
-
"./produce-
|
|
28
|
-
"./produce-
|
|
30
|
+
"./produce-assembly-file": "./lib/produce-assembly-file.js",
|
|
31
|
+
"./produce-assembly-files": "./lib/produce-assembly-files.js",
|
|
29
32
|
"./select-mutable-attributes": "./lib/select-mutable-attributes.js"
|
|
30
33
|
},
|
|
31
34
|
"imports": {
|
|
35
|
+
"#run-command": "@antora/run-command-helper",
|
|
32
36
|
"#unconvert-inline-asciidoc": "./lib/util/unconvert-inline-asciidoc.js"
|
|
33
37
|
},
|
|
34
38
|
"dependencies": {
|
|
35
39
|
"@asciidoctor/reducer": "~1.1",
|
|
36
|
-
"@antora/expand-path-helper": "~
|
|
37
|
-
"@antora/run-command-helper": "~1.0",
|
|
40
|
+
"@antora/expand-path-helper": "~3.0",
|
|
38
41
|
"braces": "~3.0",
|
|
39
42
|
"picomatch": "~3.0",
|
|
40
43
|
"js-yaml": "~4.1"
|
|
41
44
|
},
|
|
42
45
|
"devDependencies": {
|
|
43
46
|
"@antora/asciidoc-loader": "~3.1",
|
|
47
|
+
"@antora/navigation-builder": "~3.1",
|
|
44
48
|
"@antora/site-publisher": "~3.1"
|
|
45
49
|
},
|
|
46
50
|
"engines": {
|
|
@@ -54,8 +58,5 @@
|
|
|
54
58
|
"antora-extension",
|
|
55
59
|
"asciidoc",
|
|
56
60
|
"documentation"
|
|
57
|
-
]
|
|
58
|
-
"publishConfig": {
|
|
59
|
-
"access": "public"
|
|
60
|
-
}
|
|
61
|
+
]
|
|
61
62
|
}
|