@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 CHANGED
@@ -1,10 +1,11 @@
1
1
  # Antora Assembler
2
2
 
3
- A library for writing Antora extensions that combine multiple pages into one.
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 aggregate AsciiDoc documents from the pages based on the navigation tree per component version.
6
- It then invokes the specified callback to convert those documents to an output format.
7
- If a catalog is specified, Assembler pushes the converted files back into the catalog to be published as attachments alongside the other files in the site.
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.
@@ -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 produceAggregateDocuments = require('./produce-aggregate-documents')
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 runCommand = require('@antora/run-command-helper')
12
+ const { stringify: toJSON } = JSON
7
13
 
8
- async function assembleContent (playbook, contentCatalog, convertDocument, { siteCatalog, configSource }) {
9
- // Q: could we get ContentCatalog#getComponentVersionStartPage() in Antora core?
10
- if (typeof contentCatalog.getComponentVersionStartPage !== 'function') {
11
- contentCatalog.getComponentVersionStartPage = function (component, version) {
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 generatorFunctions = this ? this.getFunctions() : {}
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 aggregateDocuments = produceAggregateDocuments(loadAsciiDoc, contentCatalog, assemblerConfig)
20
- if (!convertDocument) return aggregateDocuments
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
- const buildConfig = assemblerConfig.build
23
- await prepareWorkspace(publishFiles, aggregateDocuments, contentCatalog, buildConfig)
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(aggregateDocuments.map((doc) => () => convertDocument.call(this, doc, buildConfig, runCommand)))
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 && siteCatalog) siteCatalog.addFiles(files)
31
- return files
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, aggregateDocuments, contentCatalog, buildConfig) {
37
- const { dir, clean, keepAggregateSource } = buildConfig
38
- const files = contentCatalog.findBy({ family: 'image' }).filter(({ out }) => out?.assembled)
39
- if (keepAggregateSource) {
40
- files.push(...aggregateDocuments.map((file) => Object.assign(file, { out: { path: file.path } })))
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
- // TODO: site publisher should accept a single catalog
43
- return publishFiles({ output: { clean, dir } }, [{ getFiles: () => files }])
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
@@ -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
@@ -1,5 +1,6 @@
1
1
  'use strict'
2
2
 
3
3
  const assembleContent = require('./assemble-content')
4
+ const configure = require('./configure')
4
5
 
5
- module.exports = { assembleContent }
6
+ module.exports = { assembleContent, configure, configureAssembler: configure }
@@ -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
- asciidocAttrs['loader-assembler'] = ''
33
- if (config.componentVersions == null) {
34
- config.componentVersions = ['*']
35
- } else if (typeof config.componentVersions === 'string') {
36
- config.componentVersions = config.componentVersions.split(', ')
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 (!('rootLevel' in config)) config.rootLevel = 0
39
- if (!('insertStartPage' in config)) config.insertStartPage = true
40
- if (['discrete', 'fuse', 'enclose'].indexOf(config.sectionMergeStrategy) < 0) {
41
- config.sectionMergeStrategy = 'discrete'
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 build = config.build || (config.build = {})
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 = expandPath(build.dir || './build/assembler', { dot: playbook.dir })
48
- build.cwd = playbook.dir // use playbook.dir for the purpose of finding and loading require scripts
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
- function produceAggregateDocument (
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 pagesInOutline = selectPagesInOutline(outline, pages)
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 templateFile = contentCatalog.addFile({
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
- header,
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
- return new templateFile.constructor({
45
- aggregate: true,
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
- contents: Buffer.from([...header, ...body].join('\n') + '\n'),
48
- mediaType: 'text/asciidoc',
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
- basename: path.basename(path_),
54
- stem: path.basename(path_, '.adoc'),
55
- extname: '.adoc',
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, pages) {
81
- const page = outlineEntry.urlType === 'internal' ? pages.find((it) => it.pub.url === outlineEntry.url) : undefined
82
- return (outlineEntry.items || []).reduce(
83
- (accum, item) => selectPagesInOutline(item, pages).forEach((v, k) => accum.set(k, v)) ?? accum,
84
- new Map(
85
- page && [
86
- [`${page.src.module === 'ROOT' ? '' : page.src.module + ':'}${page.src.relative}`, page],
87
- [page.pub.url, page],
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 aggregateAsciiDoc (
89
+ function mergeAsciiDoc (
94
90
  loadAsciiDoc,
95
91
  contentCatalog,
96
- header,
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.aggregated?.includes(page)) page = undefined
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 doc = loadAsciiDoc(page, contentCatalog, asciidocConfig)
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 scopeId = component !== componentVersion.name
142
- const idprefix =
143
- (scopeId ? component + ':' : '') +
144
- (module_ === 'ROOT' ? (scopeId ? ':' : '') : module_ + ':') +
145
- docnameForId +
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#${idprefix}]`)
175
+ buffer.push(`[discrete${pageFragment}]`)
181
176
  } else {
182
- buffer.push(`[#${idprefix}]`)
177
+ buffer.push(`[${pageFragment}]`)
183
178
  }
184
179
  buffer.push(`${'='.repeat(hlevel)} ${navtitleAsciiDoc}`)
185
180
  }
186
181
  } else {
187
- header.unshift(`[#${idprefix}]`)
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
- image.out.assembled = true
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
- image.out.assembled = true
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
- buffer.push(...lines.filter((it) => it !== undefined))
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.push(...resolvedAttributeEntries)
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 = contentCatalog.getFiles().find((it) => it.out && it.pub.url === url)
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
- buffer.push(
512
- ...aggregateAsciiDoc(
513
- loadAsciiDoc,
514
- contentCatalog,
515
- header,
516
- componentVersion,
517
- item,
518
- pagesInOutline,
519
- asciidocConfig,
520
- mutableAttributes,
521
- sectionMergeStrategy,
522
- lastComponentVersion,
523
- nextLevel
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(/&.+?;|[^ \p{Alpha}0-9_\-.]/gu, '')
535
- .replace(/[ _.]/g, '-')
536
- .replace(/--+/g, '-')
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
- module.exports = produceAggregateDocument
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 produceAggregateDocument = require('./produce-aggregate-document')
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 produceAggregateDocuments (loadAsciiDoc, contentCatalog, assemblerConfig) {
10
- const { insertStartPage, rootLevel, sectionMergeStrategy, asciidoc: assemblerAsciiDocConfig } = assemblerConfig
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
- return filterComponentVersions(contentCatalog.getComponents(), assemblerConfig.componentVersions).reduce(
23
+ const publishableFiles = contentCatalog.getFiles().filter((file) => file.out)
24
+ return filterComponentVersions(contentCatalog.getComponents(), assemblerConfig.componentVersionFilter.names).reduce(
17
25
  (accum, componentVersion) => {
18
- const { name: componentName, version, title, navigation } = componentVersion
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 = contentCatalog.getComponentVersionStartPage(componentName, version)
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 && !includedInNav(navigation, startPage.pub.url)) {
38
- Object.assign(rootEntry, { url: startPage.pub.url, urlType: 'internal' })
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 = createFile({
43
- component: componentVersion.name,
44
- version: componentVersion.version,
45
- relative: '.reference-page.adoc',
46
- origin: (componentVersion.origins || [])[0],
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
- accum = accum.concat(
52
- prepareOutlines(navigation, rootEntry, rootLevel).map((outline) =>
53
- produceAggregateDocument(
54
- loadAsciiDoc,
55
- contentCatalog,
56
- componentVersion,
57
- outline,
58
- doctype,
59
- contentCatalog.getPages((page) => page.out),
60
- mergedAsciiDocConfig,
61
- mutableAttributes,
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
- if (rootLevel === 0 || navigation.length === 1) {
131
- let navBranch
132
- if (navigation.length === 1) {
133
- navBranch = navigation[0]
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
- const items = navigation.reduce((accum, it) => accum.concat(it.content ? it : it.items), [])
136
- navBranch = items.length ? { items } : {}
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 rootLevel === 0 || navBranch.content ? [Object.assign(rootEntry, navBranch)] : navBranch.items
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 navigation.reduce((navTree, it) => navTree.concat(it.content ? it : it.items), [rootEntry])
135
+ return items
141
136
  }
142
137
 
143
- module.exports = produceAggregateDocuments
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.10",
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": "gitlab:antora/antora-assembler",
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-aggregate-document": "./lib/produce-aggregate-document.js",
28
- "./produce-aggregate-documents": "./lib/produce-aggregate-documents.js",
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": "~2.0",
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
  }