@antora/content-classifier 3.1.10 → 3.1.11

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.
@@ -10,50 +10,66 @@ const summarizeFileLocation = require('./util/summarize-file-location')
10
10
  *
11
11
  * @memberof content-classifier
12
12
  *
13
- * @param {Object} playbook - The configuration object for Antora.
13
+ * @param {Object} playbook - The configuration object for Antora. See ContentCatalog constructor for relevant keys.
14
14
  * @param {Object} playbook.site - Site-related configuration data.
15
15
  * @param {String} playbook.site.startPage - The start page for the site; redirects from base URL.
16
- * @param {Object} playbook.urls - URL settings for the site.
17
- * @param {String} playbook.urls.htmlExtensionStyle - The style to use when computing page URLs.
18
16
  * @param {Object} aggregate - The raw aggregate of virtual file objects to be classified.
19
17
  * @param {Object} [siteAsciiDocConfig={}] - Site-wide AsciiDoc processor configuration options.
20
- * @returns {ContentCatalog} A structured catalog of content components and virtual content files.
18
+ * @param {Function} [onComponentsRegistered] - A function (optionally async) to invoke after components are
19
+ * registered. Must return an instance of ContentCatalog. If async, this function will also return a Promise.
20
+ *
21
+ * @returns {ContentCatalog} A structured catalog of content components, versions, and virtual content files.
21
22
  */
22
- function classifyContent (playbook, aggregate, siteAsciiDocConfig = {}) {
23
- const contentCatalog = new ContentCatalog(playbook)
24
- aggregate
25
- .reduce((accum, componentVersionData) => {
26
- // drop files since they aren't needed to register component version
27
- // drop startPage to defer registration of start page
28
- const { name, version, files, startPage, ...descriptor } = Object.assign({}, componentVersionData, {
29
- asciidoc: resolveAsciiDocConfig(siteAsciiDocConfig, componentVersionData),
30
- })
31
- return new Map(accum).set(
32
- contentCatalog.registerComponentVersion(name, version, descriptor),
33
- componentVersionData
34
- )
35
- }, new Map())
36
- .forEach((componentVersionData, componentVersion) => {
37
- const { name, version } = componentVersion
38
- const { files, nav, startPage } = componentVersionData
23
+ function classifyContent (playbook, aggregate, siteAsciiDocConfig = {}, onComponentsRegistered = undefined) {
24
+ const siteStartPage = playbook.site.startPage
25
+ let contentCatalog = registerComponentVersions(new ContentCatalog(playbook), aggregate, siteAsciiDocConfig)
26
+ return typeof onComponentsRegistered === 'function' &&
27
+ (contentCatalog = onComponentsRegistered(contentCatalog)) instanceof Promise
28
+ ? contentCatalog.then((contentCatalogValue) => addFilesAndRegisterStartPages(contentCatalogValue, siteStartPage))
29
+ : addFilesAndRegisterStartPages(contentCatalog, siteStartPage)
30
+ }
31
+
32
+ function registerComponentVersions (contentCatalog, aggregate, siteAsciiDocConfig) {
33
+ for (const componentVersionBucket of aggregate) {
34
+ // advance files, nav, and startPage to component version to be used in later phase
35
+ const { name, version, files, nav, startPage, ...data } = Object.assign(componentVersionBucket, {
36
+ asciidoc: resolveAsciiDocConfig(siteAsciiDocConfig, componentVersionBucket),
37
+ })
38
+ Object.assign(contentCatalog.registerComponentVersion(name, version, data), { files, nav, startPage })
39
+ }
40
+ return contentCatalog
41
+ }
42
+
43
+ function addFilesAndRegisterStartPages (contentCatalog, siteStartPage) {
44
+ for (const { versions: componentVersions } of contentCatalog.getComponents()) {
45
+ for (const componentVersion of componentVersions) {
46
+ const { name: component, version, files = [], nav, startPage } = componentVersion
39
47
  const navResolved = nav && (nav.resolved = new Set())
40
- componentVersionData.files = undefined // clean up memory
41
- files.forEach((file) => allocateSrc(file, name, version, nav) && contentCatalog.addFile(file))
48
+ for (let file, i = 0, len = files.length; i < len; i++) {
49
+ allocateSrc((file = files[i]), component, version, nav) && contentCatalog.addFile(file, componentVersion)
50
+ files[i] = undefined // free memory
51
+ }
42
52
  if (navResolved && nav.length > navResolved.size && new Set(nav).size > navResolved.size) {
43
53
  const loc = summarizeFileLocation({ path: 'antora.yml', src: { origin: nav.origin } })
44
54
  for (const filepath of nav) {
45
55
  if (navResolved.has(filepath)) continue
46
- logger.warn('Could not resolve nav entry for %s@%s defined in %s: %s', version, name, loc, filepath)
56
+ logger.warn('Could not resolve nav entry for %s@%s defined in %s: %s', version, component, loc, filepath)
47
57
  }
48
58
  }
49
- contentCatalog.registerComponentVersionStartPage(name, componentVersion, startPage)
50
- })
51
- contentCatalog.registerSiteStartPage(playbook.site.startPage)
59
+ contentCatalog.registerComponentVersionStartPage(component, componentVersion, startPage)
60
+ }
61
+ }
62
+ contentCatalog.registerSiteStartPage(siteStartPage)
52
63
  return contentCatalog
53
64
  }
54
65
 
55
66
  function allocateSrc (file, component, version, nav) {
56
- const extname = file.src.extname
67
+ const { extname, family } = file.src
68
+ if (family && family !== 'nav') {
69
+ Object.assign(file.src, { component, version })
70
+ file.src.moduleRootPath ??= calculateRootPath(file.src.relative.split('/').length)
71
+ return true
72
+ }
57
73
  const filepath = file.path
58
74
  const pathSegments = filepath.split('/')
59
75
  let navInfo
@@ -140,13 +156,19 @@ function getNavInfo (filepath, nav) {
140
156
  if (~index) return nav.resolved.add(filepath) && { index }
141
157
  }
142
158
 
143
- function resolveAsciiDocConfig (siteAsciiDocConfig, { asciidoc, origins = [] }) {
159
+ function resolveAsciiDocConfig (siteAsciiDocConfig, { name, version, asciidoc, origins = [] }) {
144
160
  const scopedAttributes = asciidoc?.attributes
145
161
  if (scopedAttributes) {
146
- const initial = siteAsciiDocConfig.attributes
162
+ const initial = Object.assign({}, siteAsciiDocConfig.attributes)
163
+ initial['antora-component-name'] = name
164
+ initial['antora-component-version'] = version
147
165
  const mdc = { file: { path: 'antora.yml', origin: origins[origins.length - 1] } }
148
166
  const attributes = collateAsciiDocAttributes(scopedAttributes, { initial, mdc, merge: true })
149
- if (attributes !== initial) siteAsciiDocConfig = Object.assign({}, siteAsciiDocConfig, { attributes })
167
+ if (attributes !== initial) {
168
+ delete attributes['antora-component-name']
169
+ delete attributes['antora-component-version']
170
+ return Object.assign({}, siteAsciiDocConfig, { attributes })
171
+ }
150
172
  }
151
173
  return siteAsciiDocConfig
152
174
  }
@@ -5,13 +5,12 @@ const invariably = { void: () => undefined }
5
5
  const logger = require('./logger')
6
6
  const { lookup: resolveMimeType } = require('./mime-types-with-asciidoc')
7
7
  const parseResourceId = require('./util/parse-resource-id')
8
- const { posix: path } = require('path')
8
+ const { posix: path } = require('node:path')
9
9
  const resolveResource = require('./util/resolve-resource')
10
10
  const summarizeFileLocation = require('./util/summarize-file-location')
11
11
  const versionCompare = require('./util/version-compare-desc')
12
12
 
13
13
  const { ROOT_INDEX_ALIAS_ID, ROOT_INDEX_PAGE_ID } = require('./constants')
14
- const SPACE_RX = / /g
15
14
  const LOG_WRAP = '\n '
16
15
 
17
16
  const $components = Symbol('components')
@@ -24,17 +23,17 @@ class ContentCatalog {
24
23
  const urls = playbook.urls || {}
25
24
  this.htmlUrlExtensionStyle = urls.htmlExtensionStyle || 'default'
26
25
  this.urlRedirectFacility = urls.redirectFacility || 'static'
27
- this.latestVersionUrlSegment = urls.latestVersionSegment
28
- this.latestPrereleaseVersionUrlSegment = urls.latestPrereleaseVersionSegment
29
- if (this.latestVersionUrlSegment == null && this.latestPrereleaseVersionUrlSegment == null) {
30
- this.latestVersionUrlSegmentStrategy = undefined
26
+ this.latestVersionSegment = urls.latestVersionSegment
27
+ this.latestPrereleaseVersionSegment = urls.latestPrereleaseVersionSegment
28
+ if (this.latestVersionSegment == null && this.latestPrereleaseVersionSegment == null) {
29
+ this.latestVersionSegmentStrategy = undefined
31
30
  } else {
32
- this.latestVersionUrlSegmentStrategy = urls.latestVersionSegmentStrategy || 'replace'
33
- if (this.latestVersionUrlSegmentStrategy === 'redirect:from') {
34
- if (!this.latestVersionUrlSegment) this.latestVersionUrlSegment = undefined
35
- if (!this.latestPrereleaseVersionUrlSegment) {
36
- this.latestPrereleaseVersionUrlSegment = undefined
37
- if (!this.latestVersionUrlSegment) this.latestVersionUrlSegmentStrategy = undefined
31
+ this.latestVersionSegmentStrategy = urls.latestVersionSegmentStrategy || 'replace'
32
+ if (this.latestVersionSegmentStrategy === 'redirect:from') {
33
+ if (!this.latestVersionSegment) this.latestVersionSegment = undefined
34
+ if (!this.latestPrereleaseVersionSegment) {
35
+ this.latestPrereleaseVersionSegment = undefined
36
+ if (!this.latestVersionSegment) this.latestVersionSegmentStrategy = undefined
38
37
  }
39
38
  }
40
39
  }
@@ -43,6 +42,8 @@ class ContentCatalog {
43
42
  /**
44
43
  * Registers a new component version with the content catalog. Also registers the component if it does not yet exist.
45
44
  *
45
+ * Must be followed by a call to registerComponentVersionStartPage to finalize object.
46
+ *
46
47
  * @param {String} name - The name of the component to which this component version belongs.
47
48
  * @param {String} version - The version of the component to register.
48
49
  * @param {Object} [descriptor={}] - The configuration data for the component version.
@@ -59,8 +60,9 @@ class ContentCatalog {
59
60
  * @returns {Object} The constructed component version object.
60
61
  */
61
62
  registerComponentVersion (name, version, descriptor = {}) {
62
- const { asciidoc, displayVersion, prerelease, startPage: startPageSpec, title } = descriptor
63
+ const { asciidoc, displayVersion, prerelease, startPage: startPageSpec, title, versionSegment } = descriptor
63
64
  const componentVersion = { displayVersion: displayVersion || version || 'default', title: title || name, version }
65
+ if (versionSegment != null) componentVersion.versionSegment = versionSegment
64
66
  Object.defineProperty(componentVersion, 'name', { value: name, enumerable: true })
65
67
  if (prerelease) {
66
68
  componentVersion.prerelease = prerelease
@@ -129,14 +131,15 @@ class ContentCatalog {
129
131
  )
130
132
  }
131
133
  if (startPageSpec) {
134
+ // @deprecated use separate call to register start page for component version
132
135
  this.registerComponentVersionStartPage(name, componentVersion, startPageSpec === true ? undefined : startPageSpec)
133
136
  }
134
137
  return componentVersion
135
138
  }
136
139
 
137
- addFile (file) {
140
+ addFile (file, componentVersion) {
138
141
  const src = file.src
139
- let family = src.family
142
+ let { component, version, family } = src
140
143
  let filesForFamily = this[$files].get(family)
141
144
  if (!filesForFamily) this[$files].set(family, (filesForFamily = new Map()))
142
145
  const key = generateKey(src)
@@ -148,30 +151,30 @@ class ContentCatalog {
148
151
  .map((it, idx) => `${idx + 1}: ${summarizeFileLocation(it)}`)
149
152
  .join(LOG_WRAP)
150
153
  if (family === 'nav') {
151
- throw new Error(`Duplicate nav in ${src.version}@${src.component}: ${file.path}${LOG_WRAP}${details}`)
154
+ throw new Error(`Duplicate nav file: ${file.path} in ${version}@${component}${LOG_WRAP}${details}`)
152
155
  }
153
156
  throw new Error(`Duplicate ${family}: ${generateResourceSpec(src)}${LOG_WRAP}${details}`)
154
157
  }
155
- // NOTE: if the path property is not set, assume the src likely needs to be prepared
156
- // another option is to assume that if the file is not a vinyl object, the src likely needs to be prepared
158
+ // NOTE: assume that if the file is not a vinyl object, the src likely needs to be prepared
157
159
  // a vinyl object is one indication the file was created and prepared by the content aggregator
158
- //if (!src.path) prepareSrc(src)
159
- //if (!File.isVinyl(file)) file = new File(file)
160
+ // an alternate approach would be to call prepareSrc if the path property is not set
160
161
  if (!File.isVinyl(file)) {
161
162
  prepareSrc(src)
162
163
  file = new File(file)
163
164
  }
164
165
  if (family === 'alias') {
165
- src.mediaType = 'text/asciidoc'
166
166
  file.mediaType = 'text/html'
167
167
  // NOTE: an alias masquerades as the target file
168
168
  family = file.rel.src.family
169
- // QUESTION: should we preserve the mediaType property on file if already defined?
169
+ // NOTE: short circuit in case of splat alias (alias -> alias)
170
+ if (family === 'alias' && file.pub?.splat) return filesForFamily.set(key, file) && file
171
+ src.mediaType = 'text/asciidoc'
170
172
  } else if (!(file.mediaType = src.mediaType) && !('mediaType' in src)) {
173
+ // QUESTION: should we preserve the mediaType property on file if already defined?
171
174
  file.mediaType = src.mediaType = resolveMimeType(src.extname) || (family === 'page' ? 'text/asciidoc' : undefined)
172
175
  }
173
176
  let publishable
174
- let versionSegment
177
+ let activeVersionSegment
175
178
  if (file.out) {
176
179
  publishable = true
177
180
  } else if ('out' in file) {
@@ -181,15 +184,24 @@ class ContentCatalog {
181
184
  ('/' + src.relative).indexOf('/_') < 0
182
185
  ) {
183
186
  publishable = true
184
- versionSegment = computeVersionSegment.call(this, src.component, src.version)
185
- file.out = computeOut(src, family, versionSegment, this.htmlUrlExtensionStyle)
187
+ if (componentVersion == null) componentVersion = this.getComponentVersion(component, version) || { version }
188
+ activeVersionSegment =
189
+ 'activeVersionSegment' in componentVersion
190
+ ? componentVersion.activeVersionSegment
191
+ : computeVersionSegment.call(this, componentVersion)
192
+ file.out = computeOut(src, family, activeVersionSegment, this.htmlUrlExtensionStyle)
186
193
  }
187
194
  if (!file.pub && (publishable || family === 'nav')) {
188
- if (versionSegment == null) versionSegment = computeVersionSegment.call(this, src.component, src.version)
189
- file.pub = computePub(src, file.out, family, versionSegment, this.htmlUrlExtensionStyle)
195
+ if (activeVersionSegment == null) {
196
+ if (componentVersion == null) componentVersion = this.getComponentVersion(component, version) || { version }
197
+ activeVersionSegment =
198
+ 'activeVersionSegment' in componentVersion
199
+ ? componentVersion.activeVersionSegment
200
+ : computeVersionSegment.call(this, componentVersion)
201
+ }
202
+ file.pub = computePub(src, file.out, family, activeVersionSegment, this.htmlUrlExtensionStyle)
190
203
  }
191
- filesForFamily.set(key, file)
192
- return file
204
+ return filesForFamily.set(key, file) && file
193
205
  }
194
206
 
195
207
  removeFile (file) {
@@ -266,38 +278,48 @@ class ContentCatalog {
266
278
 
267
279
  // TODO add `follow` argument to control whether alias is followed
268
280
  getSiteStartPage () {
269
- return this.getById(ROOT_INDEX_PAGE_ID) || this.getById(ROOT_INDEX_ALIAS_ID)?.rel
281
+ let file
282
+ if ((file = this.getById(ROOT_INDEX_PAGE_ID))) return file
283
+ if ((file = this.getById(ROOT_INDEX_ALIAS_ID))) return file.rel
284
+ const rootComponent = this.getComponent('ROOT')
285
+ if (!rootComponent) return
286
+ const version = rootComponent.versions.find(({ activeVersionSegment }) => activeVersionSegment === '')?.version
287
+ if (!version) return
288
+ if ((file = this.getById(Object.assign({}, ROOT_INDEX_PAGE_ID, { version })))) return file
289
+ if ((file = this.getById(Object.assign({}, ROOT_INDEX_ALIAS_ID, { version })))) return file.rel
270
290
  }
271
291
 
272
292
  registerComponentVersionStartPage (name, componentVersion, startPageSpec = undefined) {
293
+ const component = name
273
294
  let version = componentVersion.version
274
295
  if (version == null) {
275
296
  // QUESTION: should we warn or throw error if component version cannot be found?
276
- if (!(componentVersion = this.getComponentVersion(name, componentVersion))) return
297
+ if (!(componentVersion = this.getComponentVersion(component, componentVersion))) return
277
298
  version = componentVersion.version
278
299
  }
300
+ const activeVersionSegment = computeVersionSegment.call(this, componentVersion)
279
301
  let startPage
280
302
  let startPageSrc
281
- const indexPageId = Object.assign({}, ROOT_INDEX_PAGE_ID, { component: name, version })
303
+ const indexPageId = Object.assign({}, ROOT_INDEX_PAGE_ID, { component, version })
282
304
  if (startPageSpec) {
283
305
  if (
284
306
  (startPage = this.resolvePage(startPageSpec, indexPageId)) &&
285
- (startPageSrc = startPage.src).component === name &&
307
+ (startPageSrc = startPage.src).component === component &&
286
308
  startPageSrc.version === version
287
309
  ) {
288
310
  if (!this.getById(indexPageId)) {
289
- const indexAliasId = Object.assign({}, ROOT_INDEX_ALIAS_ID, { component: name, version })
311
+ const indexAliasId = Object.assign({}, ROOT_INDEX_ALIAS_ID, { component, version })
290
312
  const indexAlias = this.getById(indexAliasId)
291
313
  indexAlias
292
314
  ? indexAlias.synthetic && Object.assign(indexAlias, { rel: startPage })
293
- : this.addFile({ src: indexAliasId, rel: startPage, synthetic: true })
315
+ : this.addFile({ src: indexAliasId, rel: startPage, synthetic: true }, componentVersion)
294
316
  }
295
317
  } else {
296
318
  // TODO pass componentVersion as logObject
297
319
  logger.warn(
298
320
  'Start page specified for %s@%s %s: %s',
299
321
  version,
300
- name,
322
+ component,
301
323
  startPage === false ? 'has invalid syntax' : 'not found',
302
324
  startPageSpec
303
325
  )
@@ -308,25 +330,34 @@ class ContentCatalog {
308
330
  }
309
331
  if (startPage) {
310
332
  componentVersion.url = startPage.pub.url
311
- } else {
333
+ } else if (!componentVersion.url) {
312
334
  // QUESTION: should we warn if the default start page cannot be found?
313
- const versionSegment = computeVersionSegment.call(this, name, version)
314
335
  componentVersion.url = computePub(
315
336
  (startPageSrc = prepareSrc(Object.assign({}, indexPageId, { family: 'page' }))),
316
- computeOut(startPageSrc, startPageSrc.family, versionSegment, this.htmlUrlExtensionStyle),
337
+ computeOut(startPageSrc, startPageSrc.family, activeVersionSegment, this.htmlUrlExtensionStyle),
317
338
  startPageSrc.family,
318
- versionSegment,
339
+ activeVersionSegment,
319
340
  this.htmlUrlExtensionStyle
320
341
  ).url
321
342
  }
322
-
323
- const symbolicVersionAlias = createSymbolicVersionAlias(
324
- name,
325
- version,
326
- computeVersionSegment.call(this, name, version, 'alias'),
327
- this.latestVersionUrlSegmentStrategy
328
- )
329
- if (symbolicVersionAlias) this.addFile(symbolicVersionAlias)
343
+ Object.defineProperties(componentVersion, {
344
+ activeVersionSegment:
345
+ activeVersionSegment === version
346
+ ? { configurable: true, enumerable: false, get: getVersion }
347
+ : { configurable: true, enumerable: false, value: activeVersionSegment },
348
+ files: {
349
+ configurable: true,
350
+ enumerable: false,
351
+ get: getComponentVersionFiles.bind(this, { component, version }),
352
+ },
353
+ startPage: {
354
+ configurable: true,
355
+ enumerable: false,
356
+ get: getComponentVersionStartPage.bind(this, { component, version }),
357
+ },
358
+ })
359
+ addSymbolicVersionAlias.call(this, componentVersion)
360
+ return startPage
330
361
  }
331
362
 
332
363
  registerSiteStartPage (startPageSpec) {
@@ -334,9 +365,9 @@ class ContentCatalog {
334
365
  const rel = this.resolvePage(startPageSpec)
335
366
  if (rel) {
336
367
  if (this.getById(ROOT_INDEX_PAGE_ID)) return
368
+ if (rel.pub.url === (this.htmlUrlExtensionStyle === 'default' ? '/index.html' : '/')) return
337
369
  const rootIndexAlias = this.getById(ROOT_INDEX_ALIAS_ID)
338
370
  if (rootIndexAlias) return rootIndexAlias.synthetic ? Object.assign(rootIndexAlias, { rel }) : undefined
339
- if (rel.pub.url === (this.htmlUrlExtensionStyle === 'default' ? '/index.html' : '/')) return
340
371
  const src = Object.assign({}, ROOT_INDEX_ALIAS_ID)
341
372
  return this.addFile({ src, rel, synthetic: true }, { version: src.version })
342
373
  }
@@ -357,9 +388,14 @@ class ContentCatalog {
357
388
  // QUESTION should we throw an error if alias is invalid?
358
389
  if (!src || (inferredSpec && src.relative === '.adoc')) return
359
390
  const component = this.getComponent(src.component)
391
+ let componentVersion
360
392
  if (component) {
361
393
  // NOTE version is not set when alias specifies a component, but not a version
362
- if (src.version == null) src.version = component.latest.version
394
+ if (src.version == null) {
395
+ src.version = (componentVersion = component.latest).version
396
+ } else {
397
+ componentVersion = this.getComponentVersion(component, src.version)
398
+ }
363
399
  const existingPage = this.getById(src)
364
400
  if (existingPage) {
365
401
  throw new Error(
@@ -384,33 +420,77 @@ class ContentCatalog {
384
420
  )
385
421
  }
386
422
  // NOTE the redirect producer will populate contents when the redirect facility is 'static'
387
- const alias = this.addFile({ src, rel: target })
423
+ const alias = this.addFile({ src, rel: target }, componentVersion)
388
424
  // NOTE record the first alias this target claims as the preferred one
389
425
  if (!target.rel) target.rel = alias
390
426
  return alias
391
427
  }
392
428
 
393
429
  /**
394
- * Attempts to resolve a string contextual page ID spec to a file in the catalog.
430
+ * Adds a splat (directory) alias from the specified version segment in one component to the specified
431
+ * version segment in the same or different component.
432
+ *
433
+ * @returns {File} The virtual file that represents the splat alias.
434
+ */
435
+ addSplatAlias (from, to) {
436
+ if (!from.versionSegment) throw new Error('cannot map splat alias from empty version segment')
437
+ const family = 'alias'
438
+ const baseSrc = { module: 'ROOT', family, relative: '', basename: '', stem: '', extname: '' }
439
+ const basePub = { splat: true }
440
+ const { component: fromComponent = to.component, versionSegment: fromVersionSegment } = from
441
+ const fromSrc = Object.assign({ component: fromComponent, version: fromVersionSegment }, baseSrc)
442
+ const fromPub = Object.assign(computePub(fromSrc, computeOut(fromSrc, family, fromVersionSegment), family), basePub)
443
+ const { component: toComponent, version: toVersion } = to
444
+ const toVersionSegment =
445
+ to.versionSegment ?? this.getComponentVersion(toComponent, toVersion)?.activeVersionSegment ?? toVersion
446
+ const toSrc = Object.assign({ component: toComponent, version: toVersion ?? toVersionSegment }, baseSrc)
447
+ const toPub = Object.assign(computePub(toSrc, computeOut(toSrc, family, toVersionSegment), family), basePub)
448
+ return this.addFile({ pub: fromPub, src: fromSrc, rel: { pub: toPub, src: toSrc } })
449
+ }
450
+
451
+ /**
452
+ * Attempts to resolve a page reference within the given context to a page in the catalog.
395
453
  *
396
- * Parses the specified contextual page ID spec into a page ID object, then attempts to lookup a
397
- * file with this page ID in the catalog. If a component is specified, but not a version, the
398
- * latest version of the component stored in the catalog is used. If a page cannot be resolved,
399
- * the search is attempted again for an "alias". If neither a page or alias can be resolved, the
400
- * function returns undefined. If the spec does not match the page ID syntax, this function throws
401
- * an error.
454
+ * Parses the specified page reference (i.e., page ID spec) into a partial page ID, expands it
455
+ * using the provided context, then attempts to locate a file in the page family with that page ID
456
+ * in this catalog. The family segment is optional.
402
457
  *
403
- * @param {String} spec - The contextual page ID spec (e.g., version@component:module:topic/page.adoc).
404
- * @param {ContentCatalog} catalog - The content catalog in which to resolve the page file.
405
- * @param {Object} [ctx={}] - The context to use to qualified the contextual page ID.
458
+ * If a component is specified, but no version, the latest version of the component stored in the
459
+ * catalog is used. If a page cannot be resolved, the search is attempted again for an "alias". If
460
+ * neither a page or alias can be resolved, the function returns undefined. If the syntax of the
461
+ * reference is invalid, this function throws an error.
406
462
  *
407
- * @returns {File} The virtual file to which the contextual page ID spec refers, or undefined if the
408
- * file cannot be resolved.
463
+ * @param {String} spec - The contextual page reference (e.g., version@component:module:topic/page.adoc).
464
+ * @param {Object} [context={}] - The context to use to qualify the page reference.
465
+ *
466
+ * @returns {File} The virtual file to which the contextual page reference resolves, or undefined
467
+ * if the file cannot be resolved.
409
468
  */
410
469
  resolvePage (spec, context = {}) {
411
470
  return this.resolveResource(spec, context, 'page', ['page'])
412
471
  }
413
472
 
473
+ /**
474
+ * Attempts to resolve a resource reference within the given context to a file in the catalog.
475
+ *
476
+ * Parses the specified resource reference (i.e., resource ID spec) into a partial resource ID,
477
+ * expands it using the provided context, then attempts to locate a file with that resource ID in
478
+ * this catalog.
479
+ *
480
+ * If a component is specified, but no version, the latest version of the component stored in the
481
+ * catalog is used. If a defaultFamily is not specified, the family must be specified either by
482
+ * the reference or the context. If permittedFamilies are stated, the family must resolve to a
483
+ * family in this list. If a file cannot be resolved, the function returns undefined. If the
484
+ * syntax of the reference is invalid, this function throws an error.
485
+ *
486
+ * @param {String} spec - The contextual resource reference (e.g., version@component:module:image$topic/image.png).
487
+ * @param {Object} [context={}] - The context to use to qualify the resource reference.
488
+ * @param {String} [defaultFamily=undefined] - The default family to use if one is not provided.
489
+ * @param {Array<String>} [permittedFamilies=undefined] - A list of families that are permitted.
490
+ *
491
+ * @returns {File} The virtual file to which the contextual resource reference resolves, or
492
+ * undefined if the file cannot be resolved.
493
+ */
414
494
  resolveResource (spec, context = {}, defaultFamily = undefined, permittedFamilies = undefined) {
415
495
  return resolveResource(spec, this, context, defaultFamily, permittedFamilies)
416
496
  }
@@ -452,32 +532,28 @@ function generateResourceSpec ({ component, version, module: module_, family, re
452
532
 
453
533
  function prepareSrc (src) {
454
534
  let { basename, extname, stem } = src
455
- let update
456
535
  if (basename == null) {
457
- update = true
458
- basename = path.basename(src.relative)
536
+ basename = src.basename = path.basename(src.relative)
459
537
  }
460
538
  if (stem == null) {
461
- update = true
462
539
  if (extname == null) {
463
540
  if (~(extname = basename.lastIndexOf('.'))) {
464
- stem = basename.substr(0, extname)
465
- extname = basename.substr(extname)
541
+ src.stem = basename.substr(0, extname)
542
+ src.extname = basename.substr(extname)
466
543
  } else {
467
- stem = basename
468
- extname = ''
544
+ src.stem = basename
545
+ src.extname = ''
469
546
  }
470
547
  } else {
471
- stem = basename.substr(0, basename.length - extname.length)
548
+ src.stem = basename.substr(0, basename.length - extname.length)
472
549
  }
473
550
  } else if (extname == null) {
474
- update = true
475
- extname = basename.substr(stem.length)
551
+ src.extname = basename.substr(stem.length)
476
552
  }
477
- return update ? Object.assign(src, { basename, extname, stem }) : src
553
+ return src
478
554
  }
479
555
 
480
- function computeOut (src, family, version, htmlUrlExtensionStyle) {
556
+ function computeOut (src, family, versionSegment, htmlUrlExtensionStyle) {
481
557
  let { component, module: module_, basename, extname, relative, stem } = src
482
558
  if (component === 'ROOT') component = ''
483
559
  if (module_ === 'ROOT') module_ = ''
@@ -497,7 +573,7 @@ function computeOut (src, family, version, htmlUrlExtensionStyle) {
497
573
  familyPathSegment = '_attachments'
498
574
  }
499
575
 
500
- const modulePath = path.join(component, version, module_)
576
+ const modulePath = path.join(component, versionSegment, module_)
501
577
  const dirname = path.join(modulePath, familyPathSegment, path.dirname(relative), indexifyPathSegment)
502
578
  const path_ = path.join(dirname, basename)
503
579
  const moduleRootPath = path.relative(dirname, modulePath) || '.'
@@ -506,13 +582,13 @@ function computeOut (src, family, version, htmlUrlExtensionStyle) {
506
582
  return { dirname, basename, path: path_, moduleRootPath, rootPath }
507
583
  }
508
584
 
509
- function computePub (src, out, family, version, htmlUrlExtensionStyle) {
585
+ function computePub (src, out, family, versionSegment, htmlUrlExtensionStyle) {
510
586
  const pub = {}
511
587
  let url
512
588
  if (family === 'nav') {
513
589
  const component = src.component || 'ROOT'
514
590
  const urlSegments = component === 'ROOT' ? [] : [component]
515
- if (version) urlSegments.push(version)
591
+ if (versionSegment) urlSegments.push(versionSegment)
516
592
  const module_ = src.module || 'ROOT'
517
593
  if (module_ !== 'ROOT') urlSegments.push(module_)
518
594
  if (urlSegments.length) urlSegments.push('')
@@ -531,71 +607,61 @@ function computePub (src, out, family, version, htmlUrlExtensionStyle) {
531
607
  urlSegments[lastUrlSegmentIdx] = ''
532
608
  }
533
609
  url = '/' + urlSegments.join('/')
534
- } else {
535
- if ((url = '/' + out.path) === '/.') url = '/'
536
- if (family === 'alias' && !src.relative) pub.splat = true
537
- }
538
-
539
- pub.url = ~url.indexOf(' ') ? url.replace(SPACE_RX, '%20') : url
540
-
541
- if (out) {
542
- pub.moduleRootPath = out.moduleRootPath
543
- pub.rootPath = out.rootPath
610
+ } else if ((url = '/' + out.path) === '/.') {
611
+ url = '/'
544
612
  }
613
+ pub.url = ~url.indexOf(' ') ? url.replaceAll(' ', '%20') : url
614
+ return out ? Object.assign(pub, { moduleRootPath: out.moduleRootPath, rootPath: out.rootPath }) : pub
615
+ }
545
616
 
546
- return pub
617
+ function addSymbolicVersionAlias (componentVersion) {
618
+ const { name: component, version } = componentVersion
619
+ const originalVersionSegment = computeVersionSegment.call(this, componentVersion, 'original')
620
+ const symbolicVersionSegment = computeVersionSegment.call(this, componentVersion, 'alias')
621
+ if (symbolicVersionSegment === originalVersionSegment || symbolicVersionSegment == null) return
622
+ const originalVersionSrc = { component, version, versionSegment: originalVersionSegment }
623
+ const symbolicVersionSrc = { component, version, versionSegment: symbolicVersionSegment }
624
+ return this.latestVersionSegmentStrategy === 'redirect:to'
625
+ ? this.addSplatAlias(originalVersionSrc, symbolicVersionSrc)
626
+ : this.addSplatAlias(symbolicVersionSrc, originalVersionSrc)
547
627
  }
548
628
 
549
- function computeVersionSegment (name, version, mode) {
629
+ function computeVersionSegment (componentVersion, mode) {
630
+ const version = componentVersion.version
550
631
  // special designation for master version is @deprecated; special designation scheduled to be removed in Antora 4
551
- if (mode === 'original') return !version || version === 'master' ? '' : version
552
- const strategy = this.latestVersionUrlSegmentStrategy
553
- if (!version || version === 'master') {
554
- if (mode !== 'alias') return ''
632
+ const normalizedVersion = version && version !== 'master' ? version : ''
633
+ const { versionSegment = normalizedVersion } = componentVersion
634
+ if (mode === 'original') return versionSegment
635
+ const strategy = this.latestVersionSegmentStrategy
636
+ if (!versionSegment) {
637
+ if (!mode) return ''
555
638
  if (strategy === 'redirect:to') return
556
639
  }
557
- if (strategy === 'redirect:to' || strategy === (mode === 'alias' ? 'redirect:from' : 'replace')) {
558
- const component = this.getComponent(name)
559
- const componentVersion = component && this.getComponentVersion(component, version)
560
- if (componentVersion) {
561
- const segment =
640
+ if (strategy === 'redirect:to' || strategy === (mode ? 'redirect:from' : 'replace')) {
641
+ let component
642
+ if ((component = 'name' in componentVersion && this.getComponent(componentVersion.name))) {
643
+ const latestSegment =
562
644
  componentVersion === component.latest
563
- ? this.latestVersionUrlSegment
645
+ ? this.latestVersionSegment
564
646
  : componentVersion === component.latestPrerelease
565
- ? this.latestPrereleaseVersionUrlSegment
647
+ ? this.latestPrereleaseVersionSegment
566
648
  : undefined
567
- return segment == null ? version : segment
649
+ return latestSegment == null ? versionSegment : latestSegment
568
650
  }
569
651
  }
570
- return version
652
+ return versionSegment
571
653
  }
572
654
 
573
- function createSymbolicVersionAlias (component, version, symbolicVersionSegment, strategy) {
574
- if (symbolicVersionSegment == null || symbolicVersionSegment === version) return
575
- const family = 'alias'
576
- const baseVersionAliasSrc = { component, module: 'ROOT', family, relative: '', basename: '', stem: '', extname: '' }
577
- const symbolicVersionAliasSrc = Object.assign({}, baseVersionAliasSrc, { version: symbolicVersionSegment })
578
- const symbolicVersionAlias = {
579
- src: symbolicVersionAliasSrc,
580
- pub: computePub(
581
- symbolicVersionAliasSrc,
582
- computeOut(symbolicVersionAliasSrc, family, symbolicVersionSegment),
583
- family
584
- ),
585
- }
586
- const originalVersionAliasSrc = Object.assign({}, baseVersionAliasSrc, { version })
587
- const originalVersionSegment = computeVersionSegment(component, version, 'original')
588
- const originalVersionAlias = {
589
- src: originalVersionAliasSrc,
590
- pub: computePub(
591
- originalVersionAliasSrc,
592
- computeOut(originalVersionAliasSrc, family, originalVersionSegment),
593
- family
594
- ),
595
- }
596
- return strategy === 'redirect:to'
597
- ? Object.assign(originalVersionAlias, { out: undefined, rel: symbolicVersionAlias })
598
- : Object.assign(symbolicVersionAlias, { out: undefined, rel: originalVersionAlias })
655
+ function getComponentVersionFiles (componentVersionId) {
656
+ return this.findBy(componentVersionId)
657
+ }
658
+
659
+ function getComponentVersionStartPage (componentVersionId) {
660
+ return this.resolvePage('index.adoc', componentVersionId)
661
+ }
662
+
663
+ function getVersion () {
664
+ return this.version
599
665
  }
600
666
 
601
667
  module.exports = ContentCatalog
@@ -1,6 +1,6 @@
1
1
  'use strict'
2
2
 
3
- const { posix: path } = require('path')
3
+ const { posix: path } = require('node:path')
4
4
 
5
5
  function summarizeFileLocation ({ path: path_, src: { abspath, origin } }) {
6
6
  if (!origin) return abspath || path_
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antora/content-classifier",
3
- "version": "3.1.10",
3
+ "version": "3.1.11",
4
4
  "description": "Organizes aggregated content into a virtual file catalog for use in an Antora documentation pipeline.",
5
5
  "license": "MPL-2.0",
6
6
  "author": "OpenDevise Inc. (https://opendevise.com)",
@@ -12,7 +12,8 @@
12
12
  "homepage": "https://antora.org",
13
13
  "repository": {
14
14
  "type": "git",
15
- "url": "git+https://gitlab.com/antora/antora.git"
15
+ "url": "git+https://gitlab.com/antora/antora.git",
16
+ "directory": "packages/content-classifier"
16
17
  },
17
18
  "bugs": {
18
19
  "url": "https://gitlab.com/antora/antora/issues"
@@ -30,13 +31,13 @@
30
31
  "#constants": "./lib/constants.js"
31
32
  },
32
33
  "dependencies": {
33
- "@antora/asciidoc-loader": "3.1.10",
34
- "@antora/logger": "3.1.10",
34
+ "@antora/asciidoc-loader": "3.1.11",
35
+ "@antora/logger": "3.1.11",
35
36
  "mime-types": "~2.1",
36
37
  "vinyl": "~3.0"
37
38
  },
38
39
  "engines": {
39
- "node": ">=16.0.0"
40
+ "node": ">=18.0.0"
40
41
  },
41
42
  "files": [
42
43
  "lib/"
@@ -51,7 +52,7 @@
51
52
  ],
52
53
  "scripts": {
53
54
  "test": "_mocha",
54
- "prepublishOnly": "npx -y downdoc --prepublish",
55
- "postpublish": "npx -y downdoc --postpublish"
55
+ "prepublishOnly": "npx -y downdoc@latest --prepublish",
56
+ "postpublish": "npx -y downdoc@latest --postpublish"
56
57
  }
57
58
  }