@antora/content-classifier 3.2.0-alpha.8 → 3.2.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,6 +4,7 @@ const ContentCatalog = require('./content-catalog')
4
4
  const collateAsciiDocAttributes = require('@antora/asciidoc-loader/config/collate-asciidoc-attributes')
5
5
  const logger = require('./logger')
6
6
  const summarizeFileLocation = require('./util/summarize-file-location')
7
+ const families = { attachments: true, examples: true, images: true, pages: true, partials: true }
7
8
 
8
9
  /**
9
10
  * Organizes the raw aggregate of virtual files into a {ContentCatalog}.
@@ -45,9 +46,21 @@ function addFilesAndRegisterStartPages (contentCatalog, siteStartPage) {
45
46
  for (const componentVersion of componentVersions) {
46
47
  const { name: component, version, files = [], nav, startPage } = componentVersion
47
48
  const navResolved = nav && (nav.resolved = new Set())
48
- for (let file, i = 0, len = files.length; i < len; i++) {
49
- allocateSrc((file = files[i]), component, version, nav) && contentCatalog.addFile(file, componentVersion)
49
+ const rootFiles = []
50
+ for (let file, i = 0; (file = files[i]); i++) {
50
51
  files[i] = undefined // free memory
52
+ const outcome = allocateSrc(file, component, version, componentVersion, nav)
53
+ if (outcome) {
54
+ contentCatalog.addFile(file)
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, componentVersion, null, true) && contentCatalog.addFile(file)
63
+ }
51
64
  }
52
65
  if (navResolved && nav.length > navResolved.size && new Set(nav).size > navResolved.size) {
53
66
  const loc = summarizeFileLocation({ path: 'antora.yml', src: { origin: nav.origin } })
@@ -63,21 +76,24 @@ function addFilesAndRegisterStartPages (contentCatalog, siteStartPage) {
63
76
  return contentCatalog
64
77
  }
65
78
 
66
- function allocateSrc (file, component, version, nav) {
67
- const { extname, family } = file.src
79
+ function allocateSrc (file, component, version, componentVersion, nav, implicitRoot) {
80
+ const { extname, family, origin } = file.src
68
81
  if (family && family !== 'nav') {
69
- Object.assign(file.src, { component, version })
82
+ Object.assign(file.src, { component, version, componentVersion })
83
+ if (!('private' in file) && ~('/' + file.src.relative).indexOf('/_')) file.private = true
70
84
  file.src.moduleRootPath ??= calculateRootPath(file.src.relative.split('/').length)
71
85
  return true
72
86
  }
73
87
  const filepath = file.path
74
- const pathSegments = filepath.split('/')
88
+ const pathSegments = implicitRoot ? ['modules', 'ROOT'].concat(filepath.split('/')) : filepath.split('/')
89
+ const inModules = pathSegments[0] === 'modules'
90
+ if (!implicitRoot && origin) origin.hasModulesDir ||= inModules
75
91
  let navInfo
76
92
  if (nav && (navInfo = getNavInfo(filepath, nav))) {
77
- if (extname !== '.adoc') return // ignore file
93
+ if (extname !== '.adoc') return false // ignore file
78
94
  file.nav = navInfo
79
95
  file.src.family = 'nav'
80
- if (pathSegments[0] === 'modules' && pathSegments.length > 2) {
96
+ if (inModules && pathSegments.length > 2) {
81
97
  file.src.module = pathSegments[1]
82
98
  // relative to modules/<module>
83
99
  file.src.relative = pathSegments.slice(2).join('/')
@@ -86,7 +102,7 @@ function allocateSrc (file, component, version, nav) {
86
102
  // relative to content source root
87
103
  file.src.relative = filepath
88
104
  }
89
- } else if (pathSegments[0] === 'modules') {
105
+ } else if (inModules) {
90
106
  let familyFolder = pathSegments[2]
91
107
  switch (familyFolder) {
92
108
  case 'pages':
@@ -100,45 +116,47 @@ function allocateSrc (file, component, version, nav) {
100
116
  // relative to modules/<module>/pages
101
117
  file.src.relative = pathSegments.slice(3).join('/')
102
118
  } else {
103
- return // ignore file
119
+ return false // ignore file
104
120
  }
105
121
  break
106
122
  case 'assets':
107
123
  switch ((familyFolder = pathSegments[3])) {
108
124
  case 'attachments':
109
125
  case 'images':
110
- if (!extname) return // ignore file
111
- file.src.family = familyFolder.substr(0, familyFolder.length - 1)
126
+ if (!extname && familyFolder === 'images') return false // ignore file
127
+ file.src.family = familyFolder.substring(0, familyFolder.length - 1)
112
128
  // relative to modules/<module>/assets/<family>s
113
129
  file.src.relative = pathSegments.slice(4).join('/')
114
130
  break
115
131
  default:
116
- return // ignore file
132
+ return false // ignore file
117
133
  }
118
134
  break
119
- case 'attachments':
120
135
  case 'images':
121
- if (!extname) return
122
- file.src.family = familyFolder.substr(0, familyFolder.length - 1)
136
+ if (!extname) return false
137
+ file.src.family = familyFolder.substring(0, familyFolder.length - 1)
123
138
  // relative to modules/<module>/<family>s
124
139
  file.src.relative = pathSegments.slice(3).join('/')
125
140
  break
141
+ case 'attachments':
126
142
  case 'examples':
127
143
  case 'partials':
128
- file.src.family = familyFolder.substr(0, familyFolder.length - 1)
144
+ file.src.family = familyFolder.substring(0, familyFolder.length - 1)
129
145
  // relative to modules/<module>/<family>s
130
146
  file.src.relative = pathSegments.slice(3).join('/')
131
147
  break
132
148
  default:
133
- return // ignore file
149
+ return false // ignore file
134
150
  }
135
151
  file.src.module = pathSegments[1]
136
152
  file.src.moduleRootPath = calculateRootPath(pathSegments.length - 3)
153
+ if (!('private' in file) && ~('/' + file.src.relative).indexOf('/_')) file.private = true
154
+ } else if (origin && pathSegments[0] in families) {
155
+ return // defer file
137
156
  } else {
138
- return // ignore file
157
+ return false // ignore file
139
158
  }
140
- file.src.component = component
141
- file.src.version = version
159
+ Object.assign(file.src, { component, version, componentVersion })
142
160
  return true
143
161
  }
144
162
 
@@ -152,7 +170,7 @@ function allocateSrc (file, component, version, nav) {
152
170
  * index, if this file is a navigation file, or undefined if it's not.
153
171
  */
154
172
  function getNavInfo (filepath, nav) {
155
- const index = nav.findIndex((candidate) => candidate === filepath)
173
+ const index = nav.indexOf(filepath)
156
174
  if (~index) return nav.resolved.add(filepath) && { index }
157
175
  }
158
176
 
@@ -11,7 +11,6 @@ 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')
@@ -21,6 +20,7 @@ class ContentCatalog {
21
20
  constructor (playbook = {}) {
22
21
  this[$components] = new Map()
23
22
  this[$files] = new Map()
23
+ this.publishableFamilies = new Set(['page', 'image', 'attachment'])
24
24
  const urls = playbook.urls || {}
25
25
  this.htmlUrlExtensionStyle = urls.htmlExtensionStyle || 'default'
26
26
  this.urlRedirectFacility = urls.redirectFacility || 'static'
@@ -61,8 +61,13 @@ class ContentCatalog {
61
61
  * @returns {Object} The constructed component version object.
62
62
  */
63
63
  registerComponentVersion (name, version, descriptor = {}) {
64
- const { asciidoc, displayVersion, prerelease, startPage: startPageSpec, title, versionSegment } = descriptor
65
- const componentVersion = { displayVersion: displayVersion || version || 'default', title: title || name, version }
64
+ const { asciidoc, displayVersion, prerelease, startPage: startPageRef, title, versionSegment, origins } = descriptor
65
+ const componentVersion = {
66
+ displayVersion: displayVersion || version || 'default',
67
+ title: title || name,
68
+ version,
69
+ origins: new Set(origins && typeof origins[Symbol.iterator] === 'function' ? origins : []),
70
+ }
66
71
  if (versionSegment != null) componentVersion.versionSegment = versionSegment
67
72
  Object.defineProperty(componentVersion, 'name', { value: name, enumerable: true })
68
73
  if (prerelease) {
@@ -131,18 +136,18 @@ class ContentCatalog {
131
136
  )
132
137
  )
133
138
  }
134
- if (startPageSpec) {
139
+ if (startPageRef) {
135
140
  // @deprecated use separate call to register start page for component version
136
- this.registerComponentVersionStartPage(name, componentVersion, startPageSpec === true ? undefined : startPageSpec)
141
+ this.registerComponentVersionStartPage(name, componentVersion, startPageRef === true ? undefined : startPageRef)
137
142
  }
138
143
  return componentVersion
139
144
  }
140
145
 
141
- addFile (file, componentVersion) {
146
+ addFile (file) {
142
147
  const src = file.src
143
- let { component, version, family } = src
144
- let filesForFamily = this[$files].get(family)
145
- if (!filesForFamily) this[$files].set(family, (filesForFamily = new Map()))
148
+ const family = src.family
149
+ let filesForFamily
150
+ if ((filesForFamily = this[$files].get(family)) == null) this[$files].set(family, (filesForFamily = new Map()))
146
151
  const key = generateKey(src)
147
152
  if (filesForFamily.has(key)) {
148
153
  if (family === 'alias') {
@@ -152,53 +157,73 @@ class ContentCatalog {
152
157
  .map((it, idx) => `${idx + 1}: ${summarizeFileLocation(it)}`)
153
158
  .join(LOG_WRAP)
154
159
  if (family === 'nav') {
155
- throw new Error(`Duplicate nav file: ${file.path} in ${version}@${component}${LOG_WRAP}${details}`)
160
+ throw new Error(`Duplicate nav file: ${file.path} in ${src.version}@${src.component}${LOG_WRAP}${details}`)
156
161
  }
157
162
  throw new Error(`Duplicate ${family}: ${generateResourceSpec(src)}${LOG_WRAP}${details}`)
158
163
  }
159
- // NOTE: if the path property is not set, assume the src likely needs to be prepared
160
- // another option is to assume that if the file is not a vinyl object, the src likely needs to be prepared
164
+ if (!(file.created && File.isVinyl(file))) file = this.createFile(file)
165
+ filesForFamily.set(key, file)
166
+ return file
167
+ }
168
+
169
+ createFile (file) {
170
+ const src = file.src
171
+ let { componentVersion, family } = src
172
+ if (componentVersion) delete src.componentVersion
173
+ // NOTE: assume that if the file is not a vinyl object, the src likely needs to be prepared
161
174
  // a vinyl object is one indication the file was created and prepared by the content aggregator
162
- //if (!src.path) prepareSrc(src)
163
- //if (!File.isVinyl(file)) file = new File(file)
175
+ // an alternate approach would be to call inflateSrc if the path property is not set
164
176
  if (!File.isVinyl(file)) {
165
- prepareSrc(src)
177
+ inflateSrc(src)
166
178
  file = new File(file)
167
179
  }
180
+ Object.defineProperty(file, 'created', { configurable: true, writable: true, value: true })
168
181
  if (family === 'alias') {
169
182
  file.mediaType = 'text/html'
170
- // NOTE: an alias masquerades as the target file
171
- family = file.rel.src.family
172
183
  // NOTE: short circuit in case of splat alias (alias -> alias)
173
- if (family === 'alias' && file.pub?.splat) return filesForFamily.set(key, file) && file
184
+ if (file.rel.src.family === 'alias' && file.pub?.splat) return file
185
+ // NOTE the effective family of an alias is a page, which redirects to the target file
186
+ family = 'page'
174
187
  src.mediaType = 'text/asciidoc'
175
188
  } else if (!(file.mediaType = src.mediaType) && !('mediaType' in src)) {
176
189
  // QUESTION: should we preserve the mediaType property on file if already defined?
177
- file.mediaType = src.mediaType = resolveMimeType(src.extname) || (family === 'page' ? 'text/asciidoc' : undefined)
190
+ file.mediaType = src.mediaType = src.extname
191
+ ? resolveMimeType(src.extname) || (family === 'page' ? 'text/asciidoc' : undefined)
192
+ : 'text/plain'
178
193
  }
179
- let publishable
180
- let activeVersionSegment
194
+ let activeVersionSegment = false
195
+ let publishable, referenceable
181
196
  if (file.out) {
182
197
  publishable = true
183
198
  } else if ('out' in file) {
184
199
  delete file.out
185
- } else if (
186
- (family === 'page' || family === 'image' || family === 'attachment') &&
187
- ('/' + src.relative).indexOf('/_') < 0
188
- ) {
200
+ } else if (!file.private && this.publishableFamilies.has(family)) {
189
201
  publishable = true
190
- if (componentVersion == null) componentVersion = this.getComponentVersion(component, version) || { version }
191
- activeVersionSegment = computeVersionSegment.call(this, componentVersion)
192
- file.out = computeOut(src, family, activeVersionSegment, this.htmlUrlExtensionStyle)
202
+ componentVersion ??= this.getComponentVersion(src.component, src.version) || { version: src.version }
203
+ activeVersionSegment =
204
+ 'activeVersionSegment' in componentVersion
205
+ ? componentVersion.activeVersionSegment
206
+ : computeVersionSegment.call(this, componentVersion)
207
+ file.out = computeOut.call(this, src, family, activeVersionSegment)
208
+ }
209
+ if (!file.pub) {
210
+ if ('pub' in file) {
211
+ delete file.pub
212
+ } else if (publishable || family === 'nav') {
213
+ referenceable = true
214
+ }
193
215
  }
194
- if (!file.pub && (publishable || family === 'nav')) {
195
- if (activeVersionSegment == null) {
196
- if (componentVersion == null) componentVersion = this.getComponentVersion(component, version) || { version }
197
- activeVersionSegment = computeVersionSegment.call(this, componentVersion)
216
+ if (referenceable) {
217
+ if (activeVersionSegment === false) {
218
+ componentVersion ??= this.getComponentVersion(src.component, src.version) || { version: src.version }
219
+ activeVersionSegment =
220
+ 'activeVersionSegment' in componentVersion
221
+ ? componentVersion.activeVersionSegment
222
+ : computeVersionSegment.call(this, componentVersion)
198
223
  }
199
- file.pub = computePub(src, file.out, family, activeVersionSegment, this.htmlUrlExtensionStyle)
224
+ file.pub = computePub.call(this, src, file.out, family, activeVersionSegment)
200
225
  }
201
- return filesForFamily.set(key, file) && file
226
+ return file
202
227
  }
203
228
 
204
229
  removeFile (file) {
@@ -254,21 +279,27 @@ class ContentCatalog {
254
279
  return this.getComponents().sort((a, b) => a[property].localeCompare(b[property]))
255
280
  }
256
281
 
257
- getFiles () {
258
- const accum = []
259
- for (const filesForFamily of this[$files].values()) {
260
- for (const file of filesForFamily.values()) accum.push(file)
282
+ getFiles (filter) {
283
+ const files = []
284
+ if (filter) {
285
+ for (const filesForFamily of this[$files].values()) {
286
+ for (const candidate of filesForFamily.values()) filter(candidate) && files.push(candidate)
287
+ }
288
+ } else {
289
+ for (const filesForFamily of this[$files].values()) {
290
+ for (const file of filesForFamily.values()) files.push(file)
291
+ }
261
292
  }
262
- return accum
293
+ return files
263
294
  }
264
295
 
265
296
  getPages (filter) {
266
297
  const candidates = this[$files].get('page')
267
298
  if (!candidates) return []
268
299
  if (filter) {
269
- const accum = []
270
- for (const candidate of candidates.values()) filter(candidate) && accum.push(candidate)
271
- return accum
300
+ const pages = []
301
+ for (const candidate of candidates.values()) filter(candidate) && pages.push(candidate)
302
+ return pages
272
303
  }
273
304
  return [...candidates.values()]
274
305
  }
@@ -286,17 +317,14 @@ class ContentCatalog {
286
317
  if ((file = this.getById(Object.assign({}, ROOT_INDEX_ALIAS_ID, { version })))) return file.rel
287
318
  }
288
319
 
289
- registerComponentVersionStartPage (name, componentVersion, startPageSpec = undefined) {
290
- const component = name
291
- let version = componentVersion.version
292
- if (version == null) {
320
+ registerComponentVersionStartPage (component, componentVersion, startPageSpec = undefined) {
321
+ if (componentVersion.constructor === String) {
293
322
  // QUESTION: should we warn or throw error if component version cannot be found?
294
323
  if (!(componentVersion = this.getComponentVersion(component, componentVersion))) return
295
- version = componentVersion.version
296
324
  }
325
+ const version = componentVersion.version
297
326
  const activeVersionSegment = computeVersionSegment.call(this, componentVersion)
298
- let startPage
299
- let startPageSrc
327
+ let startPage, startPageSrc
300
328
  const indexPageId = Object.assign({}, ROOT_INDEX_PAGE_ID, { component, version })
301
329
  if (startPageSpec) {
302
330
  if (
@@ -305,11 +333,11 @@ class ContentCatalog {
305
333
  startPageSrc.version === version
306
334
  ) {
307
335
  if (!this.getById(indexPageId)) {
308
- const indexAliasId = Object.assign({}, ROOT_INDEX_ALIAS_ID, { component, version })
336
+ const indexAliasId = Object.assign({}, ROOT_INDEX_ALIAS_ID, { component, version, componentVersion })
309
337
  const indexAlias = this.getById(indexAliasId)
310
338
  indexAlias
311
339
  ? indexAlias.synthetic && Object.assign(indexAlias, { rel: startPage })
312
- : this.addFile({ src: indexAliasId, rel: startPage, synthetic: true }, componentVersion)
340
+ : this.addFile({ src: indexAliasId, rel: startPage, synthetic: true })
313
341
  }
314
342
  } else {
315
343
  // TODO pass componentVersion as logObject
@@ -328,20 +356,21 @@ class ContentCatalog {
328
356
  if (startPage) {
329
357
  componentVersion.url = startPage.pub.url
330
358
  } else if (!componentVersion.url) {
359
+ startPageSrc = inflateSrc(Object.assign({}, indexPageId, { family: 'page' }))
331
360
  // QUESTION: should we warn if the default start page cannot be found?
332
- componentVersion.url = computePub(
333
- (startPageSrc = prepareSrc(Object.assign({}, indexPageId, { family: 'page' }))),
334
- computeOut(startPageSrc, startPageSrc.family, activeVersionSegment, this.htmlUrlExtensionStyle),
361
+ componentVersion.url = computePub.call(
362
+ this,
363
+ startPageSrc,
364
+ computeOut.call(this, startPageSrc, startPageSrc.family, activeVersionSegment),
335
365
  startPageSrc.family,
336
- activeVersionSegment,
337
- this.htmlUrlExtensionStyle
366
+ activeVersionSegment
338
367
  ).url
339
368
  }
340
369
  Object.defineProperties(componentVersion, {
341
370
  activeVersionSegment:
342
371
  activeVersionSegment === version
343
- ? { configurable: true, enumerable: false, get: getVersion }
344
- : { configurable: true, enumerable: false, value: activeVersionSegment },
372
+ ? { configurable: true, get: getVersion }
373
+ : { configurable: true, value: activeVersionSegment },
345
374
  files: {
346
375
  configurable: true,
347
376
  enumerable: false,
@@ -366,7 +395,7 @@ class ContentCatalog {
366
395
  const rootIndexAlias = this.getById(ROOT_INDEX_ALIAS_ID)
367
396
  if (rootIndexAlias) return rootIndexAlias.synthetic ? Object.assign(rootIndexAlias, { rel }) : undefined
368
397
  const src = Object.assign({}, ROOT_INDEX_ALIAS_ID)
369
- return this.addFile({ src, rel, synthetic: true }, { version: src.version })
398
+ return this.addFile({ src, rel, synthetic: true })
370
399
  }
371
400
  if (rel === false) {
372
401
  logger.warn('Start page specified for site has invalid syntax: %s', startPageSpec)
@@ -385,14 +414,9 @@ class ContentCatalog {
385
414
  // QUESTION should we throw an error if alias is invalid?
386
415
  if (!src || (inferredSpec && src.relative === '.adoc')) return
387
416
  const component = this.getComponent(src.component)
388
- let componentVersion
389
417
  if (component) {
390
418
  // NOTE version is not set when alias specifies a component, but not a version
391
- if (src.version == null) {
392
- src.version = (componentVersion = component.latest).version
393
- } else {
394
- componentVersion = this.getComponentVersion(component, src.version)
395
- }
419
+ src.version ??= (src.componentVersion = component.latest).version
396
420
  const existingPage = this.getById(src)
397
421
  if (existingPage) {
398
422
  throw new Error(
@@ -417,7 +441,7 @@ class ContentCatalog {
417
441
  )
418
442
  }
419
443
  // NOTE the redirect producer will populate contents when the redirect facility is 'static'
420
- const alias = this.addFile({ src, rel: target }, componentVersion)
444
+ const alias = this.addFile({ src, rel: target })
421
445
  // NOTE record the first alias this target claims as the preferred one
422
446
  if (!target.rel) target.rel = alias
423
447
  return alias
@@ -436,36 +460,64 @@ class ContentCatalog {
436
460
  const basePub = { splat: true }
437
461
  const { component: fromComponent = to.component, versionSegment: fromVersionSegment } = from
438
462
  const fromSrc = Object.assign({ component: fromComponent, version: fromVersionSegment }, baseSrc)
439
- const fromPub = Object.assign(computePub(fromSrc, computeOut(fromSrc, family, fromVersionSegment), family), basePub)
463
+ const fromPub = Object.assign(
464
+ computePub.call(this, fromSrc, computeOut.call(this, fromSrc, family, fromVersionSegment), family),
465
+ basePub
466
+ )
440
467
  const { component: toComponent, version: toVersion } = to
441
468
  const toVersionSegment =
442
469
  to.versionSegment ?? this.getComponentVersion(toComponent, toVersion)?.activeVersionSegment ?? toVersion
443
470
  const toSrc = Object.assign({ component: toComponent, version: toVersion ?? toVersionSegment }, baseSrc)
444
- const toPub = Object.assign(computePub(toSrc, computeOut(toSrc, family, toVersionSegment), family), basePub)
471
+ const toPub = Object.assign(
472
+ computePub.call(this, toSrc, computeOut.call(this, toSrc, family, toVersionSegment), family),
473
+ basePub
474
+ )
445
475
  return this.addFile({ pub: fromPub, src: fromSrc, rel: { pub: toPub, src: toSrc } })
446
476
  }
447
477
 
448
478
  /**
449
- * Attempts to resolve a string contextual page ID spec to a file in the catalog.
479
+ * Attempts to resolve a page reference within the given context to a page in the catalog.
480
+ *
481
+ * Parses the specified page reference (i.e., page ID spec) into a partial page ID, expands it
482
+ * using the provided context, then attempts to locate a file in the page family with that page ID
483
+ * in this catalog. The family segment is optional.
450
484
  *
451
- * Parses the specified contextual page ID spec into a page ID object, then attempts to lookup a
452
- * file with this page ID in the catalog. If a component is specified, but not a version, the
453
- * latest version of the component stored in the catalog is used. If a page cannot be resolved,
454
- * the search is attempted again for an "alias". If neither a page or alias can be resolved, the
455
- * function returns undefined. If the spec does not match the page ID syntax, this function throws
456
- * an error.
485
+ * If a component is specified, but no version, the latest version of the component stored in the
486
+ * catalog is used. If a page cannot be resolved, the search is attempted again for an "alias". If
487
+ * neither a page or alias can be resolved, the function returns undefined. If the syntax of the
488
+ * reference is invalid, this function throws an error.
457
489
  *
458
- * @param {String} spec - The contextual page ID spec (e.g., version@component:module:topic/page.adoc).
459
- * @param {ContentCatalog} catalog - The content catalog in which to resolve the page file.
460
- * @param {Object} [ctx={}] - The context to use to qualified the contextual page ID.
490
+ * @param {String} spec - The contextual page reference (e.g., version@component:module:topic/page.adoc).
491
+ * @param {Object} [context={}] - The context to use to qualify the page reference.
461
492
  *
462
- * @returns {File} The virtual file to which the contextual page ID spec refers, or undefined if the
463
- * file cannot be resolved.
493
+ * @returns {File} The virtual file to which the contextual page reference resolves, or undefined
494
+ * if the file cannot be resolved.
464
495
  */
465
496
  resolvePage (spec, context = {}) {
466
497
  return this.resolveResource(spec, context, 'page', ['page'])
467
498
  }
468
499
 
500
+ /**
501
+ * Attempts to resolve a resource reference within the given context to a file in the catalog.
502
+ *
503
+ * Parses the specified resource reference (i.e., resource ID spec) into a partial resource ID,
504
+ * expands it using the provided context, then attempts to locate a file with that resource ID in
505
+ * this catalog.
506
+ *
507
+ * If a component is specified, but no version, the latest version of the component stored in the
508
+ * catalog is used. If a defaultFamily is not specified, the family must be specified either by
509
+ * the reference or the context. If permittedFamilies are stated, the family must resolve to a
510
+ * family in this list. If a file cannot be resolved, the function returns undefined. If the
511
+ * syntax of the reference is invalid, this function throws an error.
512
+ *
513
+ * @param {String} spec - The contextual resource reference (e.g., version@component:module:image$topic/image.png).
514
+ * @param {Object} [context={}] - The context to use to qualify the resource reference.
515
+ * @param {String} [defaultFamily=undefined] - The default family to use if one is not provided.
516
+ * @param {Array<String>} [permittedFamilies=undefined] - A list of families that are permitted.
517
+ *
518
+ * @returns {File} The virtual file to which the contextual resource reference resolves, or
519
+ * undefined if the file cannot be resolved.
520
+ */
469
521
  resolveResource (spec, context = {}, defaultFamily = undefined, permittedFamilies = undefined) {
470
522
  return resolveResource(spec, this, context, defaultFamily, permittedFamilies)
471
523
  }
@@ -505,34 +557,30 @@ function generateResourceSpec ({ component, version, module: module_, family, re
505
557
  )
506
558
  }
507
559
 
508
- function prepareSrc (src) {
560
+ function inflateSrc (src) {
509
561
  let { basename, extname, stem } = src
510
- let update
511
562
  if (basename == null) {
512
- update = true
513
- basename = path.basename(src.relative)
563
+ basename = src.basename = path.basename(src.relative)
514
564
  }
515
565
  if (stem == null) {
516
- update = true
517
566
  if (extname == null) {
518
567
  if (~(extname = basename.lastIndexOf('.'))) {
519
- stem = basename.substr(0, extname)
520
- extname = basename.substr(extname)
568
+ src.stem = basename.substring(0, extname)
569
+ src.extname = basename.substring(extname)
521
570
  } else {
522
- stem = basename
523
- extname = ''
571
+ src.stem = basename
572
+ src.extname = ''
524
573
  }
525
574
  } else {
526
- stem = basename.substr(0, basename.length - extname.length)
575
+ src.stem = basename.substring(0, basename.length - extname.length)
527
576
  }
528
577
  } else if (extname == null) {
529
- update = true
530
- extname = basename.substr(stem.length)
578
+ src.extname = basename.substring(stem.length)
531
579
  }
532
- return update ? Object.assign(src, { basename, extname, stem }) : src
580
+ return src
533
581
  }
534
582
 
535
- function computeOut (src, family, versionSegment, htmlUrlExtensionStyle) {
583
+ function computeOut (src, family, versionSegment) {
536
584
  let { component, module: module_, basename, extname, relative, stem } = src
537
585
  if (component === 'ROOT') component = ''
538
586
  if (module_ === 'ROOT') module_ = ''
@@ -540,16 +588,14 @@ function computeOut (src, family, versionSegment, htmlUrlExtensionStyle) {
540
588
  let familyPathSegment = ''
541
589
 
542
590
  if (family === 'page') {
543
- if (stem !== 'index' && htmlUrlExtensionStyle === 'indexify') {
591
+ if (stem !== 'index' && this.htmlUrlExtensionStyle === 'indexify') {
544
592
  basename = 'index.html'
545
593
  indexifyPathSegment = stem
546
594
  } else if (extname === '.adoc') {
547
595
  basename = stem + '.html'
548
596
  }
549
- } else if (family === 'image') {
550
- familyPathSegment = '_images'
551
- } else if (family === 'attachment') {
552
- familyPathSegment = '_attachments'
597
+ } else if (this.publishableFamilies.has(family)) {
598
+ familyPathSegment = '_' + family + 's'
553
599
  }
554
600
 
555
601
  const modulePath = path.join(component, versionSegment, module_)
@@ -561,7 +607,7 @@ function computeOut (src, family, versionSegment, htmlUrlExtensionStyle) {
561
607
  return { dirname, basename, path: path_, moduleRootPath, rootPath }
562
608
  }
563
609
 
564
- function computePub (src, out, family, versionSegment, htmlUrlExtensionStyle) {
610
+ function computePub (src, out, family, versionSegment) {
565
611
  const pub = {}
566
612
  let url
567
613
  if (family === 'nav') {
@@ -577,19 +623,19 @@ function computePub (src, out, family, versionSegment, htmlUrlExtensionStyle) {
577
623
  } else if (family === 'page') {
578
624
  const urlSegments = out.path.split('/')
579
625
  const lastUrlSegmentIdx = urlSegments.length - 1
580
- if (htmlUrlExtensionStyle === 'drop') {
626
+ if (this.htmlUrlExtensionStyle === 'drop') {
581
627
  // drop just the .html extension or, if the filename is index.html, the whole segment
582
628
  const lastUrlSegment = urlSegments[lastUrlSegmentIdx]
583
629
  urlSegments[lastUrlSegmentIdx] =
584
- lastUrlSegment === 'index.html' ? '' : lastUrlSegment.substr(0, lastUrlSegment.length - 5)
585
- } else if (htmlUrlExtensionStyle === 'indexify') {
630
+ lastUrlSegment === 'index.html' ? '' : lastUrlSegment.substring(0, lastUrlSegment.length - 5)
631
+ } else if (this.htmlUrlExtensionStyle === 'indexify') {
586
632
  urlSegments[lastUrlSegmentIdx] = ''
587
633
  }
588
634
  url = '/' + urlSegments.join('/')
589
635
  } else if ((url = '/' + out.path) === '/.') {
590
636
  url = '/'
591
637
  }
592
- pub.url = ~url.indexOf(' ') ? url.replace(SPACE_RX, '%20') : url
638
+ pub.url = ~url.indexOf(' ') ? url.replaceAll(' ', '%20') : url
593
639
  return out ? Object.assign(pub, { moduleRootPath: out.moduleRootPath, rootPath: out.rootPath }) : pub
594
640
  }
595
641
 
@@ -59,7 +59,7 @@ function parseResourceId (spec, ctx = {}, defaultFamily = 'page', permittedFamil
59
59
  const relativeSegments = relative.split('/')
60
60
  let from
61
61
  if (relativeSegments[0] === '.' && (from = ctx.relative)) {
62
- relativeSegments[0] = from.substr(0, (from.lastIndexOf('/') + 1 || 1) - 1)
62
+ relativeSegments[0] = from.substring(0, (from.lastIndexOf('/') + 1 || 1) - 1)
63
63
  }
64
64
  relative = relativeSegments
65
65
  .reduce((accum, segment) => {
@@ -26,7 +26,7 @@ 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
- if (!id || !id.family) return false
29
+ if (!id?.family) return false
30
30
  if (id.version == null) {
31
31
  const component = catalog.getComponent(id.component)
32
32
  if (!component) return
@@ -33,7 +33,7 @@ function versionCompareDesc (a, b) {
33
33
  function resolveSemver (str) {
34
34
  const chr0 = str.charAt()
35
35
  if (chr0 === 'v') {
36
- if (isDigit(str.charAt(1)) && (str = str.substr(1)) && (~str.indexOf('.') || isDigit(str.charAt()))) {
36
+ if (isDigit(str.charAt(1)) && (str = str.substring(1)) && (~str.indexOf('.') || isDigit(str.charAt()))) {
37
37
  return str
38
38
  }
39
39
  } else if (isDigit(chr0) && (~str.indexOf('.') || isInteger(Number(str)))) {
@@ -51,12 +51,12 @@ function semverCompare (a, b) {
51
51
  const preOffsetA = a.indexOf('-')
52
52
  const preOffsetB = b.indexOf('-')
53
53
  if (~preOffsetA) {
54
- preA = a.substr(preOffsetA + 1)
55
- a = a.substr(0, preOffsetA)
54
+ preA = a.substring(preOffsetA + 1)
55
+ a = a.substring(0, preOffsetA)
56
56
  }
57
57
  if (~preOffsetB) {
58
- preB = b.substr(preOffsetB + 1)
59
- b = b.substr(0, preOffsetB)
58
+ preB = b.substring(preOffsetB + 1)
59
+ b = b.substring(0, preOffsetB)
60
60
  }
61
61
  const numsA = a.split('.')
62
62
  const numsB = b.split('.')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antora/content-classifier",
3
- "version": "3.2.0-alpha.8",
3
+ "version": "3.2.0-rc.1",
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)",
@@ -31,13 +31,13 @@
31
31
  "#constants": "./lib/constants.js"
32
32
  },
33
33
  "dependencies": {
34
- "@antora/asciidoc-loader": "3.2.0-alpha.8",
35
- "@antora/logger": "3.2.0-alpha.8",
36
- "mime-types": "~2.1",
34
+ "@antora/asciidoc-loader": "3.2.0-rc.1",
35
+ "@antora/logger": "3.2.0-rc.1",
36
+ "mime-types": "~3.0",
37
37
  "vinyl": "~3.0"
38
38
  },
39
39
  "engines": {
40
- "node": ">=18.0.0"
40
+ "node": ">=20.0.0"
41
41
  },
42
42
  "files": [
43
43
  "lib/"
@@ -51,7 +51,7 @@
51
51
  "web publishing"
52
52
  ],
53
53
  "scripts": {
54
- "test": "_mocha",
54
+ "test": "node --test",
55
55
  "prepublishOnly": "npx -y downdoc@latest --prepublish",
56
56
  "postpublish": "npx -y downdoc@latest --postpublish"
57
57
  }