@antora/content-classifier 3.2.0-alpha.1 → 3.2.0-alpha.10

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.
@@ -2,69 +2,110 @@
2
2
 
3
3
  const ContentCatalog = require('./content-catalog')
4
4
  const collateAsciiDocAttributes = require('@antora/asciidoc-loader/config/collate-asciidoc-attributes')
5
+ const logger = require('./logger')
6
+ const summarizeFileLocation = require('./util/summarize-file-location')
7
+ const families = { attachments: true, examples: true, images: true, pages: true, partials: true }
5
8
 
6
9
  /**
7
10
  * Organizes the raw aggregate of virtual files into a {ContentCatalog}.
8
11
  *
9
12
  * @memberof content-classifier
10
13
  *
11
- * @param {Object} playbook - The configuration object for Antora.
14
+ * @param {Object} playbook - The configuration object for Antora. See ContentCatalog constructor for relevant keys.
12
15
  * @param {Object} playbook.site - Site-related configuration data.
13
16
  * @param {String} playbook.site.startPage - The start page for the site; redirects from base URL.
14
- * @param {Object} playbook.urls - URL settings for the site.
15
- * @param {String} playbook.urls.htmlExtensionStyle - The style to use when computing page URLs.
16
17
  * @param {Object} aggregate - The raw aggregate of virtual file objects to be classified.
17
18
  * @param {Object} [siteAsciiDocConfig={}] - Site-wide AsciiDoc processor configuration options.
18
- * @returns {ContentCatalog} A structured catalog of content components and virtual content files.
19
+ * @param {Function} [onComponentsRegistered] - A function (optionally async) to invoke after components are
20
+ * registered. Must return an instance of ContentCatalog. If async, this function will also return a Promise.
21
+ *
22
+ * @returns {ContentCatalog} A structured catalog of content components, versions, and virtual content files.
19
23
  */
20
- function classifyContent (playbook, aggregate, siteAsciiDocConfig = {}) {
21
- const contentCatalog = new ContentCatalog(playbook)
22
- aggregate
23
- .reduce((accum, componentVersionData) => {
24
- // drop files since they aren't needed to register component version
25
- // drop startPage to defer registration of start page
26
- const { name, version, files, startPage, ...descriptor } = Object.assign({}, componentVersionData, {
27
- asciidoc: resolveAsciiDocConfig(siteAsciiDocConfig, componentVersionData),
28
- })
29
- return new Map(accum).set(
30
- contentCatalog.registerComponentVersion(name, version, descriptor),
31
- componentVersionData
32
- )
33
- }, new Map())
34
- .forEach((componentVersionData, componentVersion) => {
35
- const { name, version } = componentVersion
36
- const { files, nav, startPage } = componentVersionData
37
- componentVersionData.files = undefined // clean up memory
38
- files.forEach((file) => allocateSrc(file, name, version, nav) && contentCatalog.addFile(file, componentVersion))
39
- contentCatalog.registerComponentVersionStartPage(name, componentVersion, startPage)
24
+ function classifyContent (playbook, aggregate, siteAsciiDocConfig = {}, onComponentsRegistered = undefined) {
25
+ const siteStartPage = playbook.site.startPage
26
+ let contentCatalog = registerComponentVersions(new ContentCatalog(playbook), aggregate, siteAsciiDocConfig)
27
+ return typeof onComponentsRegistered === 'function' &&
28
+ (contentCatalog = onComponentsRegistered(contentCatalog)) instanceof Promise
29
+ ? contentCatalog.then((contentCatalogValue) => addFilesAndRegisterStartPages(contentCatalogValue, siteStartPage))
30
+ : addFilesAndRegisterStartPages(contentCatalog, siteStartPage)
31
+ }
32
+
33
+ function registerComponentVersions (contentCatalog, aggregate, siteAsciiDocConfig) {
34
+ for (const componentVersionBucket of aggregate) {
35
+ // advance files, nav, and startPage to component version to be used in later phase
36
+ const { name, version, files, nav, startPage, ...data } = Object.assign(componentVersionBucket, {
37
+ asciidoc: resolveAsciiDocConfig(siteAsciiDocConfig, componentVersionBucket),
40
38
  })
41
- contentCatalog.registerSiteStartPage(playbook.site.startPage)
39
+ Object.assign(contentCatalog.registerComponentVersion(name, version, data), { files, nav, startPage })
40
+ }
41
+ return contentCatalog
42
+ }
43
+
44
+ function addFilesAndRegisterStartPages (contentCatalog, siteStartPage) {
45
+ for (const { versions: componentVersions } of contentCatalog.getComponents()) {
46
+ for (const componentVersion of componentVersions) {
47
+ const { name: component, version, files = [], nav, startPage } = componentVersion
48
+ const navResolved = nav && (nav.resolved = new Set())
49
+ const rootFiles = []
50
+ for (let file, i = 0; (file = files[i]); i++) {
51
+ files[i] = undefined // free memory
52
+ const outcome = allocateSrc(file, component, version, nav)
53
+ if (outcome) {
54
+ contentCatalog.addFile(file, componentVersion)
55
+ } else if (outcome == null) {
56
+ rootFiles.push(file)
57
+ }
58
+ }
59
+ if (rootFiles.length) {
60
+ for (let file, i = 0; (file = rootFiles[i]); i++) {
61
+ if (file.src.origin.hasModulesDir) continue
62
+ allocateSrc(file, component, version, null, true) && contentCatalog.addFile(file, componentVersion)
63
+ }
64
+ }
65
+ if (navResolved && nav.length > navResolved.size && new Set(nav).size > navResolved.size) {
66
+ const loc = summarizeFileLocation({ path: 'antora.yml', src: { origin: nav.origin } })
67
+ for (const filepath of nav) {
68
+ if (navResolved.has(filepath)) continue
69
+ logger.warn('Could not resolve nav entry for %s@%s defined in %s: %s', version, component, loc, filepath)
70
+ }
71
+ }
72
+ contentCatalog.registerComponentVersionStartPage(component, componentVersion, startPage)
73
+ }
74
+ }
75
+ contentCatalog.registerSiteStartPage(siteStartPage)
42
76
  return contentCatalog
43
77
  }
44
78
 
45
- function allocateSrc (file, component, version, nav) {
46
- const extname = file.src.extname
79
+ function allocateSrc (file, component, version, nav, implicitRoot) {
80
+ const { extname, family, origin } = file.src
81
+ if (family && family !== 'nav') {
82
+ Object.assign(file.src, { component, version })
83
+ file.src.moduleRootPath ??= calculateRootPath(file.src.relative.split('/').length)
84
+ return true
85
+ }
47
86
  const filepath = file.path
48
- const navInfo = nav && getNavInfo(filepath, nav)
49
- const pathSegments = filepath.split('/')
50
- if (navInfo) {
51
- if (extname !== '.adoc') return // ignore file
87
+ const pathSegments = implicitRoot ? ['modules', 'ROOT'].concat(filepath.split('/')) : filepath.split('/')
88
+ const inModules = pathSegments[0] === 'modules'
89
+ if (!implicitRoot && origin) origin.hasModulesDir ||= inModules
90
+ let navInfo
91
+ if (nav && (navInfo = getNavInfo(filepath, nav))) {
92
+ if (extname !== '.adoc') return false // ignore file
52
93
  file.nav = navInfo
53
94
  file.src.family = 'nav'
54
- if (pathSegments[0] === 'modules' && pathSegments.length > 2) {
95
+ if (inModules && pathSegments.length > 2) {
55
96
  file.src.module = pathSegments[1]
56
97
  // relative to modules/<module>
57
98
  file.src.relative = pathSegments.slice(2).join('/')
58
99
  file.src.moduleRootPath = calculateRootPath(pathSegments.length - 3)
59
100
  } else {
60
- // relative to root
101
+ // relative to content source root
61
102
  file.src.relative = filepath
62
103
  }
63
- } else if (pathSegments[0] === 'modules') {
104
+ } else if (inModules) {
64
105
  let familyFolder = pathSegments[2]
65
106
  switch (familyFolder) {
66
107
  case 'pages':
67
- // pages/_partials location for partials is @deprecated; special designation scheduled to be removed in Antora 4
108
+ // pages/_partials location for partials is @deprecated; special designation scheduled for removal in Antora 4
68
109
  if (pathSegments[3] === '_partials') {
69
110
  file.src.family = 'partial'
70
111
  // relative to modules/<module>/pages/_partials
@@ -74,29 +115,29 @@ function allocateSrc (file, component, version, nav) {
74
115
  // relative to modules/<module>/pages
75
116
  file.src.relative = pathSegments.slice(3).join('/')
76
117
  } else {
77
- return // ignore file
118
+ return false // ignore file
78
119
  }
79
120
  break
80
121
  case 'assets':
81
122
  switch ((familyFolder = pathSegments[3])) {
82
123
  case 'attachments':
83
124
  case 'images':
84
- if (!extname) return // ignore file
125
+ if (!extname && familyFolder === 'images') return false // ignore file
85
126
  file.src.family = familyFolder.substr(0, familyFolder.length - 1)
86
127
  // relative to modules/<module>/assets/<family>s
87
128
  file.src.relative = pathSegments.slice(4).join('/')
88
129
  break
89
130
  default:
90
- return // ignore file
131
+ return false // ignore file
91
132
  }
92
133
  break
93
- case 'attachments':
94
134
  case 'images':
95
- if (!extname) return
135
+ if (!extname) return false
96
136
  file.src.family = familyFolder.substr(0, familyFolder.length - 1)
97
137
  // relative to modules/<module>/<family>s
98
138
  file.src.relative = pathSegments.slice(3).join('/')
99
139
  break
140
+ case 'attachments':
100
141
  case 'examples':
101
142
  case 'partials':
102
143
  file.src.family = familyFolder.substr(0, familyFolder.length - 1)
@@ -104,12 +145,14 @@ function allocateSrc (file, component, version, nav) {
104
145
  file.src.relative = pathSegments.slice(3).join('/')
105
146
  break
106
147
  default:
107
- return // ignore file
148
+ return false // ignore file
108
149
  }
109
150
  file.src.module = pathSegments[1]
110
151
  file.src.moduleRootPath = calculateRootPath(pathSegments.length - 3)
152
+ } else if (origin && pathSegments[0] in families) {
153
+ return // defer file
111
154
  } else {
112
- return // ignore file
155
+ return false // ignore file
113
156
  }
114
157
  file.src.component = component
115
158
  file.src.version = version
@@ -127,16 +170,22 @@ function allocateSrc (file, component, version, nav) {
127
170
  */
128
171
  function getNavInfo (filepath, nav) {
129
172
  const index = nav.findIndex((candidate) => candidate === filepath)
130
- if (~index) return { index }
173
+ if (~index) return nav.resolved.add(filepath) && { index }
131
174
  }
132
175
 
133
- function resolveAsciiDocConfig (siteAsciiDocConfig, { asciidoc, origins = [] }) {
134
- const scopedAttributes = (asciidoc || {}).attributes
176
+ function resolveAsciiDocConfig (siteAsciiDocConfig, { name, version, asciidoc, origins = [] }) {
177
+ const scopedAttributes = asciidoc?.attributes
135
178
  if (scopedAttributes) {
136
- const initial = siteAsciiDocConfig.attributes
179
+ const initial = Object.assign({}, siteAsciiDocConfig.attributes)
180
+ initial['antora-component-name'] = name
181
+ initial['antora-component-version'] = version
137
182
  const mdc = { file: { path: 'antora.yml', origin: origins[origins.length - 1] } }
138
183
  const attributes = collateAsciiDocAttributes(scopedAttributes, { initial, mdc, merge: true })
139
- if (attributes !== initial) siteAsciiDocConfig = Object.assign({}, siteAsciiDocConfig, { attributes })
184
+ if (attributes !== initial) {
185
+ delete attributes['antora-component-name']
186
+ delete attributes['antora-component-version']
187
+ return Object.assign({}, siteAsciiDocConfig, { attributes })
188
+ }
140
189
  }
141
190
  return siteAsciiDocConfig
142
191
  }
@@ -5,12 +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
+ const summarizeFileLocation = require('./util/summarize-file-location')
10
11
  const versionCompare = require('./util/version-compare-desc')
11
12
 
12
13
  const { ROOT_INDEX_ALIAS_ID, ROOT_INDEX_PAGE_ID } = require('./constants')
13
- const SPACE_RX = / /g
14
14
  const LOG_WRAP = '\n '
15
15
 
16
16
  const $components = Symbol('components')
@@ -42,6 +42,8 @@ class ContentCatalog {
42
42
  /**
43
43
  * Registers a new component version with the content catalog. Also registers the component if it does not yet exist.
44
44
  *
45
+ * Must be followed by a call to registerComponentVersionStartPage to finalize object.
46
+ *
45
47
  * @param {String} name - The name of the component to which this component version belongs.
46
48
  * @param {String} version - The version of the component to register.
47
49
  * @param {Object} [descriptor={}] - The configuration data for the component version.
@@ -58,8 +60,13 @@ class ContentCatalog {
58
60
  * @returns {Object} The constructed component version object.
59
61
  */
60
62
  registerComponentVersion (name, version, descriptor = {}) {
61
- const { asciidoc, displayVersion, prerelease, startPage: startPageSpec, title, versionSegment } = descriptor
62
- const componentVersion = { displayVersion: displayVersion || version || 'default', title: title || name, version }
63
+ const { asciidoc, displayVersion, prerelease, startPage: startPageRef, title, versionSegment, origins } = descriptor
64
+ const componentVersion = {
65
+ displayVersion: displayVersion || version || 'default',
66
+ title: title || name,
67
+ version,
68
+ origins: new Set(origins && typeof origins[Symbol.iterator] === 'function' ? origins : []),
69
+ }
63
70
  if (versionSegment != null) componentVersion.versionSegment = versionSegment
64
71
  Object.defineProperty(componentVersion, 'name', { value: name, enumerable: true })
65
72
  if (prerelease) {
@@ -73,6 +80,7 @@ class ContentCatalog {
73
80
  }
74
81
  }
75
82
  }
83
+ // NOTE if no AsciiDoc attributes are defined in the component descriptor, asciidoc is the siteAsciiDocConfig object
76
84
  if (asciidoc) componentVersion.asciidoc = asciidoc
77
85
  const component = this[$components].get(name)
78
86
  if (component) {
@@ -83,10 +91,10 @@ class ContentCatalog {
83
91
  let lastVerdict
84
92
  const insertIdx = version
85
93
  ? componentVersions.findIndex(({ version: candidateVersion, prerelease: candidatePrerelease }) => {
86
- return (lastVerdict = versionCompare(candidateVersion, version)) > 1
87
- ? !!prerelease === !!candidatePrerelease
88
- : lastVerdict > 0 || (lastVerdict < -1 && prerelease && !candidatePrerelease)
89
- })
94
+ return (lastVerdict = versionCompare(candidateVersion, version)) > 1
95
+ ? !!prerelease === !!candidatePrerelease
96
+ : lastVerdict > 0 || (lastVerdict < -1 && prerelease && !candidatePrerelease)
97
+ })
90
98
  : prerelease
91
99
  ? -1
92
100
  : ~(~componentVersions.findIndex(({ prerelease: candidatePrerelease }) => !candidatePrerelease) || -1)
@@ -127,9 +135,9 @@ class ContentCatalog {
127
135
  )
128
136
  )
129
137
  }
130
- if (startPageSpec) {
138
+ if (startPageRef) {
131
139
  // @deprecated use separate call to register start page for component version
132
- this.registerComponentVersionStartPage(name, componentVersion, startPageSpec === true ? undefined : startPageSpec)
140
+ this.registerComponentVersionStartPage(name, componentVersion, startPageRef === true ? undefined : startPageRef)
133
141
  }
134
142
  return componentVersion
135
143
  }
@@ -143,36 +151,34 @@ class ContentCatalog {
143
151
  if (filesForFamily.has(key)) {
144
152
  if (family === 'alias') {
145
153
  throw new Error(`Duplicate alias: ${generateResourceSpec(src)}`)
146
- } else {
147
- const details = [filesForFamily.get(key), file]
148
- .map((it, idx) => `${idx + 1}: ${getFileLocation(it)}`)
149
- .join(LOG_WRAP)
150
- if (family === 'nav') {
151
- throw new Error(`Duplicate nav in ${version}@${component}: ${file.path}${LOG_WRAP}${details}`)
152
- } else {
153
- throw new Error(`Duplicate ${family}: ${generateResourceSpec(src)}${LOG_WRAP}${details}`)
154
- }
155
154
  }
155
+ const details = [filesForFamily.get(key), file]
156
+ .map((it, idx) => `${idx + 1}: ${summarizeFileLocation(it)}`)
157
+ .join(LOG_WRAP)
158
+ if (family === 'nav') {
159
+ throw new Error(`Duplicate nav file: ${file.path} in ${version}@${component}${LOG_WRAP}${details}`)
160
+ }
161
+ throw new Error(`Duplicate ${family}: ${generateResourceSpec(src)}${LOG_WRAP}${details}`)
156
162
  }
157
- // NOTE: if the path property is not set, assume the src likely needs to be prepared
158
- // another option is to assume that if the file is not a vinyl object, the src likely needs to be prepared
163
+ // NOTE: assume that if the file is not a vinyl object, the src likely needs to be prepared
159
164
  // a vinyl object is one indication the file was created and prepared by the content aggregator
160
- //if (!src.path) prepareSrc(src)
161
- //if (!File.isVinyl(file)) file = new File(file)
165
+ // an alternate approach would be to call prepareSrc if the path property is not set
162
166
  if (!File.isVinyl(file)) {
163
167
  prepareSrc(src)
164
168
  file = new File(file)
165
169
  }
166
170
  if (family === 'alias') {
167
171
  file.mediaType = 'text/html'
168
- // NOTE: an alias masquerades as the target file
169
- family = file.rel.src.family
170
172
  // NOTE: short circuit in case of splat alias (alias -> alias)
171
- if (family === 'alias' && (file.pub || {}).splat) return filesForFamily.set(key, file) && file
173
+ if (file.rel.src.family === 'alias' && file.pub?.splat) return filesForFamily.set(key, file) && file
174
+ // NOTE the effective family of an alias is a page, which redirects to the target file
175
+ family = 'page'
172
176
  src.mediaType = 'text/asciidoc'
173
177
  } else if (!(file.mediaType = src.mediaType) && !('mediaType' in src)) {
174
178
  // QUESTION: should we preserve the mediaType property on file if already defined?
175
- file.mediaType = src.mediaType = resolveMimeType(src.extname) || (family === 'page' ? 'text/asciidoc' : undefined)
179
+ file.mediaType = src.mediaType = src.extname
180
+ ? resolveMimeType(src.extname) || (family === 'page' ? 'text/asciidoc' : undefined)
181
+ : 'text/plain'
176
182
  }
177
183
  let publishable
178
184
  let activeVersionSegment
@@ -181,18 +187,25 @@ class ContentCatalog {
181
187
  } else if ('out' in file) {
182
188
  delete file.out
183
189
  } else if (
190
+ !file.private &&
184
191
  (family === 'page' || family === 'image' || family === 'attachment') &&
185
- ('/' + src.relative).indexOf('/_') < 0
192
+ (file.private === false || ('/' + src.relative).indexOf('/_') < 0)
186
193
  ) {
187
194
  publishable = true
188
195
  if (componentVersion == null) componentVersion = this.getComponentVersion(component, version) || { version }
189
- activeVersionSegment = computeVersionSegment.call(this, componentVersion)
196
+ activeVersionSegment =
197
+ 'activeVersionSegment' in componentVersion
198
+ ? componentVersion.activeVersionSegment
199
+ : computeVersionSegment.call(this, componentVersion)
190
200
  file.out = computeOut(src, family, activeVersionSegment, this.htmlUrlExtensionStyle)
191
201
  }
192
202
  if (!file.pub && (publishable || family === 'nav')) {
193
203
  if (activeVersionSegment == null) {
194
204
  if (componentVersion == null) componentVersion = this.getComponentVersion(component, version) || { version }
195
- activeVersionSegment = computeVersionSegment.call(this, componentVersion)
205
+ activeVersionSegment =
206
+ 'activeVersionSegment' in componentVersion
207
+ ? componentVersion.activeVersionSegment
208
+ : computeVersionSegment.call(this, componentVersion)
196
209
  }
197
210
  file.pub = computePub(src, file.out, family, activeVersionSegment, this.htmlUrlExtensionStyle)
198
211
  }
@@ -239,7 +252,7 @@ class ContentCatalog {
239
252
  }
240
253
 
241
254
  getComponentVersion (component, version) {
242
- return (component.versions || (this.getComponent(component) || {}).versions || []).find(
255
+ return (component.versions || this.getComponent(component)?.versions || []).find(
243
256
  ({ version: candidate }) => candidate === version
244
257
  )
245
258
  }
@@ -267,14 +280,21 @@ class ContentCatalog {
267
280
  const accum = []
268
281
  for (const candidate of candidates.values()) filter(candidate) && accum.push(candidate)
269
282
  return accum
270
- } else {
271
- return [...candidates.values()]
272
283
  }
284
+ return [...candidates.values()]
273
285
  }
274
286
 
275
287
  // TODO add `follow` argument to control whether alias is followed
276
288
  getSiteStartPage () {
277
- return this.getById(ROOT_INDEX_PAGE_ID) || (this.getById(ROOT_INDEX_ALIAS_ID) || {}).rel
289
+ let file
290
+ if ((file = this.getById(ROOT_INDEX_PAGE_ID))) return file
291
+ if ((file = this.getById(ROOT_INDEX_ALIAS_ID))) return file.rel
292
+ const rootComponent = this.getComponent('ROOT')
293
+ if (!rootComponent) return
294
+ const version = rootComponent.versions.find(({ activeVersionSegment }) => activeVersionSegment === '')?.version
295
+ if (!version) return
296
+ if ((file = this.getById(Object.assign({}, ROOT_INDEX_PAGE_ID, { version })))) return file
297
+ if ((file = this.getById(Object.assign({}, ROOT_INDEX_ALIAS_ID, { version })))) return file.rel
278
298
  }
279
299
 
280
300
  registerComponentVersionStartPage (name, componentVersion, startPageSpec = undefined) {
@@ -318,7 +338,7 @@ class ContentCatalog {
318
338
  }
319
339
  if (startPage) {
320
340
  componentVersion.url = startPage.pub.url
321
- } else {
341
+ } else if (!componentVersion.url) {
322
342
  // QUESTION: should we warn if the default start page cannot be found?
323
343
  componentVersion.url = computePub(
324
344
  (startPageSrc = prepareSrc(Object.assign({}, indexPageId, { family: 'page' }))),
@@ -328,21 +348,24 @@ class ContentCatalog {
328
348
  this.htmlUrlExtensionStyle
329
349
  ).url
330
350
  }
331
-
332
- Object.defineProperty(
333
- componentVersion,
334
- 'activeVersionSegment',
335
- activeVersionSegment === version
336
- ? {
337
- configurable: true,
338
- get () {
339
- return this.version
340
- },
341
- }
342
- : { configurable: true, value: activeVersionSegment }
343
- )
344
-
351
+ Object.defineProperties(componentVersion, {
352
+ activeVersionSegment:
353
+ activeVersionSegment === version
354
+ ? { configurable: true, enumerable: false, get: getVersion }
355
+ : { configurable: true, enumerable: false, value: activeVersionSegment },
356
+ files: {
357
+ configurable: true,
358
+ enumerable: false,
359
+ get: getComponentVersionFiles.bind(this, { component, version }),
360
+ },
361
+ startPage: {
362
+ configurable: true,
363
+ enumerable: false,
364
+ get: getComponentVersionStartPage.bind(this, { component, version }),
365
+ },
366
+ })
345
367
  addSymbolicVersionAlias.call(this, componentVersion)
368
+ return startPage
346
369
  }
347
370
 
348
371
  registerSiteStartPage (startPageSpec) {
@@ -350,11 +373,13 @@ class ContentCatalog {
350
373
  const rel = this.resolvePage(startPageSpec)
351
374
  if (rel) {
352
375
  if (this.getById(ROOT_INDEX_PAGE_ID)) return
353
- const indexAlias = this.getById(ROOT_INDEX_ALIAS_ID)
354
- if (indexAlias) return indexAlias.synthetic ? Object.assign(indexAlias, { rel }) : undefined
376
+ if (rel.pub.url === (this.htmlUrlExtensionStyle === 'default' ? '/index.html' : '/')) return
377
+ const rootIndexAlias = this.getById(ROOT_INDEX_ALIAS_ID)
378
+ if (rootIndexAlias) return rootIndexAlias.synthetic ? Object.assign(rootIndexAlias, { rel }) : undefined
355
379
  const src = Object.assign({}, ROOT_INDEX_ALIAS_ID)
356
380
  return this.addFile({ src, rel, synthetic: true }, { version: src.version })
357
- } else if (rel === false) {
381
+ }
382
+ if (rel === false) {
358
383
  logger.warn('Start page specified for site has invalid syntax: %s', startPageSpec)
359
384
  } else if (startPageSpec.lastIndexOf(':') > startPageSpec.indexOf(':')) {
360
385
  logger.warn('Start page specified for site not found: %s', startPageSpec)
@@ -384,10 +409,10 @@ class ContentCatalog {
384
409
  throw new Error(
385
410
  existingPage === target
386
411
  ? `Page cannot define alias that references itself: ${generateResourceSpec(src)}` +
387
- ` (specified as: ${spec})${LOG_WRAP}source: ${getFileLocation(existingPage)}`
412
+ ` (specified as: ${spec})${LOG_WRAP}source: ${summarizeFileLocation(existingPage)}`
388
413
  : `Page alias cannot reference an existing page: ${generateResourceSpec(src)} (specified as: ${spec})` +
389
- `${LOG_WRAP}source: ${getFileLocation(target)}` +
390
- `${LOG_WRAP}existing page: ${getFileLocation(existingPage)}`
414
+ `${LOG_WRAP}source: ${summarizeFileLocation(target)}` +
415
+ `${LOG_WRAP}existing page: ${summarizeFileLocation(existingPage)}`
391
416
  )
392
417
  }
393
418
  } else if (src.version == null) {
@@ -399,7 +424,7 @@ class ContentCatalog {
399
424
  if (existingAlias) {
400
425
  throw new Error(
401
426
  `Duplicate alias: ${generateResourceSpec(src)} (specified as: ${spec})` +
402
- `${LOG_WRAP}source: ${getFileLocation(target)}`
427
+ `${LOG_WRAP}source: ${summarizeFileLocation(target)}`
403
428
  )
404
429
  }
405
430
  // NOTE the redirect producer will populate contents when the redirect facility is 'static'
@@ -432,26 +457,48 @@ class ContentCatalog {
432
457
  }
433
458
 
434
459
  /**
435
- * Attempts to resolve a string contextual page ID spec to a file in the catalog.
460
+ * Attempts to resolve a page reference within the given context to a page in the catalog.
436
461
  *
437
- * Parses the specified contextual page ID spec into a page ID object, then attempts to lookup a
438
- * file with this page ID in the catalog. If a component is specified, but not a version, the
439
- * latest version of the component stored in the catalog is used. If a page cannot be resolved,
440
- * the search is attempted again for an "alias". If neither a page or alias can be resolved, the
441
- * function returns undefined. If the spec does not match the page ID syntax, this function throws
442
- * an error.
462
+ * Parses the specified page reference (i.e., page ID spec) into a partial page ID, expands it
463
+ * using the provided context, then attempts to locate a file in the page family with that page ID
464
+ * in this catalog. The family segment is optional.
443
465
  *
444
- * @param {String} spec - The contextual page ID spec (e.g., version@component:module:topic/page.adoc).
445
- * @param {ContentCatalog} catalog - The content catalog in which to resolve the page file.
446
- * @param {Object} [ctx={}] - The context to use to qualified the contextual page ID.
466
+ * If a component is specified, but no version, the latest version of the component stored in the
467
+ * catalog is used. If a page cannot be resolved, the search is attempted again for an "alias". If
468
+ * neither a page or alias can be resolved, the function returns undefined. If the syntax of the
469
+ * reference is invalid, this function throws an error.
447
470
  *
448
- * @returns {File} The virtual file to which the contextual page ID spec refers, or undefined if the
449
- * file cannot be resolved.
471
+ * @param {String} spec - The contextual page reference (e.g., version@component:module:topic/page.adoc).
472
+ * @param {Object} [context={}] - The context to use to qualify the page reference.
473
+ *
474
+ * @returns {File} The virtual file to which the contextual page reference resolves, or undefined
475
+ * if the file cannot be resolved.
450
476
  */
451
477
  resolvePage (spec, context = {}) {
452
478
  return this.resolveResource(spec, context, 'page', ['page'])
453
479
  }
454
480
 
481
+ /**
482
+ * Attempts to resolve a resource reference within the given context to a file in the catalog.
483
+ *
484
+ * Parses the specified resource reference (i.e., resource ID spec) into a partial resource ID,
485
+ * expands it using the provided context, then attempts to locate a file with that resource ID in
486
+ * this catalog.
487
+ *
488
+ * If a component is specified, but no version, the latest version of the component stored in the
489
+ * catalog is used. If a defaultFamily is not specified, the family must be specified either by
490
+ * the reference or the context. If permittedFamilies are stated, the family must resolve to a
491
+ * family in this list. If a file cannot be resolved, the function returns undefined. If the
492
+ * syntax of the reference is invalid, this function throws an error.
493
+ *
494
+ * @param {String} spec - The contextual resource reference (e.g., version@component:module:image$topic/image.png).
495
+ * @param {Object} [context={}] - The context to use to qualify the resource reference.
496
+ * @param {String} [defaultFamily=undefined] - The default family to use if one is not provided.
497
+ * @param {Array<String>} [permittedFamilies=undefined] - A list of families that are permitted.
498
+ *
499
+ * @returns {File} The virtual file to which the contextual resource reference resolves, or
500
+ * undefined if the file cannot be resolved.
501
+ */
455
502
  resolveResource (spec, context = {}, defaultFamily = undefined, permittedFamilies = undefined) {
456
503
  return resolveResource(spec, this, context, defaultFamily, permittedFamilies)
457
504
  }
@@ -493,29 +540,25 @@ function generateResourceSpec ({ component, version, module: module_, family, re
493
540
 
494
541
  function prepareSrc (src) {
495
542
  let { basename, extname, stem } = src
496
- let update
497
543
  if (basename == null) {
498
- update = true
499
- basename = path.basename(src.relative)
544
+ basename = src.basename = path.basename(src.relative)
500
545
  }
501
546
  if (stem == null) {
502
- update = true
503
547
  if (extname == null) {
504
548
  if (~(extname = basename.lastIndexOf('.'))) {
505
- stem = basename.substr(0, extname)
506
- extname = basename.substr(extname)
549
+ src.stem = basename.substr(0, extname)
550
+ src.extname = basename.substr(extname)
507
551
  } else {
508
- stem = basename
509
- extname = ''
552
+ src.stem = basename
553
+ src.extname = ''
510
554
  }
511
555
  } else {
512
- stem = basename.substr(0, basename.length - extname.length)
556
+ src.stem = basename.substr(0, basename.length - extname.length)
513
557
  }
514
558
  } else if (extname == null) {
515
- update = true
516
- extname = basename.substr(stem.length)
559
+ src.extname = basename.substr(stem.length)
517
560
  }
518
- return update ? Object.assign(src, { basename, extname, stem }) : src
561
+ return src
519
562
  }
520
563
 
521
564
  function computeOut (src, family, versionSegment, htmlUrlExtensionStyle) {
@@ -575,7 +618,7 @@ function computePub (src, out, family, versionSegment, htmlUrlExtensionStyle) {
575
618
  } else if ((url = '/' + out.path) === '/.') {
576
619
  url = '/'
577
620
  }
578
- pub.url = ~url.indexOf(' ') ? url.replace(SPACE_RX, '%20') : url
621
+ pub.url = ~url.indexOf(' ') ? url.replaceAll(' ', '%20') : url
579
622
  return out ? Object.assign(pub, { moduleRootPath: out.moduleRootPath, rootPath: out.rootPath }) : pub
580
623
  }
581
624
 
@@ -593,7 +636,7 @@ function addSymbolicVersionAlias (componentVersion) {
593
636
 
594
637
  function computeVersionSegment (componentVersion, mode) {
595
638
  const version = componentVersion.version
596
- // special designation for master verson is @deprecated; special designation scheduled to be removed in Antora 4
639
+ // special designation for master version is @deprecated; special designation scheduled to be removed in Antora 4
597
640
  const normalizedVersion = version && version !== 'master' ? version : ''
598
641
  const { versionSegment = normalizedVersion } = componentVersion
599
642
  if (mode === 'original') return versionSegment
@@ -617,13 +660,16 @@ function computeVersionSegment (componentVersion, mode) {
617
660
  return versionSegment
618
661
  }
619
662
 
620
- function getFileLocation ({ path: path_, src: { abspath, origin } }) {
621
- if (!origin) return abspath || path_
622
- const { url, gitdir, worktree, refname, tag, reftype = tag ? 'tag' : 'branch', remote, startPath } = origin
623
- let details = `${reftype}: ${refname}`
624
- if ('worktree' in origin) details += worktree ? ' <worktree>' : remote ? ` <remotes/${remote}>` : ''
625
- if (startPath) details += ` | start path: ${startPath}`
626
- return `${abspath || path.join(startPath, path_)} in ${'worktree' in origin ? worktree || gitdir : url} (${details})`
663
+ function getComponentVersionFiles (componentVersionId) {
664
+ return this.findBy(componentVersionId)
665
+ }
666
+
667
+ function getComponentVersionStartPage (componentVersionId) {
668
+ return this.resolvePage('index.adoc', componentVersionId)
669
+ }
670
+
671
+ function getVersion () {
672
+ return this.version
627
673
  }
628
674
 
629
675
  module.exports = ContentCatalog
@@ -26,7 +26,6 @@ const parseResourceId = require('./parse-resource-id')
26
26
  */
27
27
  function resolveResource (spec, catalog, ctx = {}, defaultFamily = undefined, permittedFamilies = undefined) {
28
28
  const id = parseResourceId(spec, ctx, defaultFamily, permittedFamilies)
29
-
30
29
  if (!id || !id.family) return false
31
30
  if (id.version == null) {
32
31
  const component = catalog.getComponent(id.component)
@@ -34,10 +33,9 @@ function resolveResource (spec, catalog, ctx = {}, defaultFamily = undefined, pe
34
33
  id.version = component.latest.version
35
34
  }
36
35
  if (!id.module) id.module = 'ROOT'
37
-
38
36
  return (
39
37
  catalog.getById(id) ||
40
- (id.family === 'page' ? (catalog.getById(Object.assign({}, id, { family: 'alias' })) || {}).rel : undefined)
38
+ (id.family === 'page' ? catalog.getById(Object.assign({}, id, { family: 'alias' }))?.rel : undefined)
41
39
  )
42
40
  }
43
41
 
@@ -0,0 +1,14 @@
1
+ 'use strict'
2
+
3
+ const { posix: path } = require('node:path')
4
+
5
+ function summarizeFileLocation ({ path: path_, src: { abspath, origin } }) {
6
+ if (!origin) return abspath || path_
7
+ const { url, gitdir, worktree, refname, tag, reftype = tag ? 'tag' : 'branch', remote, startPath } = origin
8
+ let details = `${reftype}: ${refname}`
9
+ if ('worktree' in origin) details += worktree ? ' <worktree>' : remote ? ` <remotes/${remote}>` : ''
10
+ if (startPath) details += ` | start path: ${startPath}`
11
+ return `${abspath || path.join(startPath, path_)} in ${'worktree' in origin ? worktree || gitdir : url} (${details})`
12
+ }
13
+
14
+ module.exports = summarizeFileLocation
@@ -24,11 +24,8 @@ function versionCompareDesc (a, b) {
24
24
  if (a && b) {
25
25
  const semverA = resolveSemver(a)
26
26
  const semverB = resolveSemver(b)
27
- if (semverA) {
28
- return semverB ? -semverCompare(semverA, semverB) : 1
29
- } else {
30
- return semverB ? -1 : -2 * a.localeCompare(b, 'en', { numeric: true })
31
- }
27
+ if (semverA) return semverB ? -semverCompare(semverA, semverB) : 1
28
+ return semverB ? -1 : -2 * a.localeCompare(b, 'en', { numeric: true })
32
29
  }
33
30
  return a ? 1 : -1
34
31
  }
@@ -64,23 +61,18 @@ function semverCompare (a, b) {
64
61
  const numsA = a.split('.')
65
62
  const numsB = b.split('.')
66
63
  for (let i = 0; i < 3; i++) {
67
- const numA = Number(numsA[i] || 0)
68
- const numB = Number(numsB[i] || 0)
69
- if (numA > numB) {
70
- return 1
71
- } else if (numB > numA) {
72
- return -1
73
- } else if (isNaN(numA)) {
74
- if (!isNaN(numB)) return -1
75
- } else if (isNaN(numB)) {
64
+ const numA = Number(numsA[i] ?? 0)
65
+ const numB = Number(numsB[i] ?? 0)
66
+ if (numA > numB) return 1
67
+ if (numB > numA) return -1
68
+ if (Number.isNaN(numA)) {
69
+ if (!Number.isNaN(numB)) return -1
70
+ } else if (Number.isNaN(numB)) {
76
71
  return 1
77
72
  }
78
73
  }
79
- if (preA == null) {
80
- return preB == null ? 0 : 1
81
- } else {
82
- return preB == null ? -1 : preA.localeCompare(preB, 'en', { numeric: true })
83
- }
74
+ if (preA == null) return preB == null ? 0 : 1
75
+ return preB == null ? -1 : preA.localeCompare(preB, 'en', { numeric: true })
84
76
  }
85
77
 
86
78
  module.exports = versionCompareDesc
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antora/content-classifier",
3
- "version": "3.2.0-alpha.1",
3
+ "version": "3.2.0-alpha.10",
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)",
@@ -10,7 +10,11 @@
10
10
  "Hubert SABLONNIÈRE <hubert.sablonniere@gmail.com>"
11
11
  ],
12
12
  "homepage": "https://antora.org",
13
- "repository": "gitlab:antora/antora",
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://gitlab.com/antora/antora.git",
16
+ "directory": "packages/content-classifier"
17
+ },
14
18
  "bugs": {
15
19
  "url": "https://gitlab.com/antora/antora/issues"
16
20
  },
@@ -27,13 +31,13 @@
27
31
  "#constants": "./lib/constants.js"
28
32
  },
29
33
  "dependencies": {
30
- "@antora/asciidoc-loader": "3.2.0-alpha.1",
31
- "@antora/logger": "3.2.0-alpha.1",
34
+ "@antora/asciidoc-loader": "3.2.0-alpha.10",
35
+ "@antora/logger": "3.2.0-alpha.10",
32
36
  "mime-types": "~2.1",
33
- "vinyl": "~2.2"
37
+ "vinyl": "~3.0"
34
38
  },
35
39
  "engines": {
36
- "node": ">=16.0.0"
40
+ "node": ">=18.0.0"
37
41
  },
38
42
  "files": [
39
43
  "lib/"
@@ -48,7 +52,7 @@
48
52
  ],
49
53
  "scripts": {
50
54
  "test": "_mocha",
51
- "prepublishOnly": "node $npm_config_local_prefix/npm/prepublishOnly.js",
52
- "postpublish": "node $npm_config_local_prefix/npm/postpublish.js"
55
+ "prepublishOnly": "npx -y downdoc@latest --prepublish",
56
+ "postpublish": "npx -y downdoc@latest --postpublish"
53
57
  }
54
58
  }