@antora/content-classifier 3.0.0-alpha.7 → 3.0.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  The Content Classifier is a component in Antora responsible for organizing aggregated content into a virtual file catalog for use in an Antora documentation pipeline.
4
4
 
5
5
  [Antora](https://antora.org) is a modular static site generator designed for creating documentation sites from AsciiDoc documents.
6
- Its site generator pipeline aggregates documents from versioned content repositories and processes them using [Asciidoctor](https://asciidoctor.org).
6
+ Its site generator aggregates documents from versioned content repositories and processes them using [Asciidoctor](https://asciidoctor.org).
7
7
 
8
8
  ## Copyright and License
9
9
 
package/lib/constants.js CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict'
2
2
 
3
3
  module.exports = Object.freeze({
4
- START_ALIAS_ID: Object.freeze({ component: '', version: '', module: '', family: 'alias', relative: 'index.adoc' }),
5
- START_PAGE_ID: Object.freeze({ component: '', version: '', module: '', family: 'page', relative: 'index.adoc' }),
4
+ ROOT_INDEX_ALIAS_ID: { component: 'ROOT', version: '', module: 'ROOT', family: 'alias', relative: 'index.adoc' },
5
+ ROOT_INDEX_PAGE_ID: { component: 'ROOT', version: '', module: 'ROOT', family: 'page', relative: 'index.adoc' },
6
6
  })
@@ -1,6 +1,7 @@
1
1
  'use strict'
2
2
 
3
3
  const File = require('./file')
4
+ const invariably = { void: () => undefined }
4
5
  const logger = require('./logger')
5
6
  const { lookup: resolveMimeType } = require('./mime-types-with-asciidoc')
6
7
  const parseResourceId = require('./util/parse-resource-id')
@@ -8,7 +9,7 @@ const { posix: path } = require('path')
8
9
  const resolveResource = require('./util/resolve-resource')
9
10
  const versionCompare = require('./util/version-compare-desc')
10
11
 
11
- const { START_ALIAS_ID, START_PAGE_ID } = require('./constants')
12
+ const { ROOT_INDEX_ALIAS_ID, ROOT_INDEX_PAGE_ID } = require('./constants')
12
13
  const SPACE_RX = / /g
13
14
 
14
15
  const $components = Symbol('components')
@@ -72,15 +73,17 @@ class ContentCatalog {
72
73
  const component = this[$components].get(name)
73
74
  if (component) {
74
75
  const componentVersions = component.versions
75
- const insertIdx = componentVersions.findIndex(({ version: candidate }) => {
76
- if (candidate === version) throw new Error(`Duplicate version detected for component ${name}: ${version}`)
77
- return versionCompare(candidate, version) > 0
78
- })
79
- if (~insertIdx) {
80
- componentVersions.splice(insertIdx, 0, componentVersion)
81
- } else {
82
- componentVersions.push(componentVersion)
76
+ if (componentVersions.find(({ version: candidate }) => candidate === version)) {
77
+ throw new Error(`Duplicate version detected for component ${name}: ${version}`)
83
78
  }
79
+ const insertIdx = prerelease
80
+ ? componentVersions.findIndex(({ version: candidateVersion, prerelease: candidatePrerelease }) =>
81
+ candidatePrerelease ? versionCompare(candidateVersion, version) > 0 : true
82
+ )
83
+ : componentVersions.findIndex(({ version: candidateVersion, prerelease: candidatePrerelease }) =>
84
+ candidatePrerelease ? false : versionCompare(candidateVersion, version) > 0
85
+ )
86
+ ~insertIdx ? componentVersions.splice(insertIdx, 0, componentVersion) : componentVersions.push(componentVersion)
84
87
  if ((component.latest = componentVersions.find((candidate) => !candidate.prerelease))) {
85
88
  if (componentVersions[0] !== component.latest) component.latestPrerelease = componentVersions[0]
86
89
  } else {
@@ -120,12 +123,16 @@ class ContentCatalog {
120
123
  addFile (file) {
121
124
  const src = file.src
122
125
  let family = src.family
126
+ let filesForFamily = this[$files].get(family)
127
+ if (!filesForFamily) this[$files].set(family, (filesForFamily = new Map()))
123
128
  const key = generateKey(src)
124
- if (this[$files].has(key)) {
129
+ if (filesForFamily.has(key)) {
125
130
  if (family === 'alias') {
126
131
  throw new Error(`Duplicate alias: ${generateResourceSpec(src)}`)
127
132
  } else {
128
- const details = [this.getById(src), file].map((it, idx) => ` ${idx + 1}: ${getFileLocation(it)}`).join('\n')
133
+ const details = [filesForFamily.get(key), file]
134
+ .map((it, idx) => ` ${idx + 1}: ${getFileLocation(it)}`)
135
+ .join('\n')
129
136
  if (family === 'nav') {
130
137
  throw new Error(`Duplicate nav in ${src.version}@${src.component}: ${file.path}\n${details}`)
131
138
  } else {
@@ -162,35 +169,42 @@ class ContentCatalog {
162
169
  ('/' + src.relative).indexOf('/_') < 0
163
170
  ) {
164
171
  publishable = true
165
- versionSegment = computeVersionSegment.bind(this)(src.component, src.version)
172
+ versionSegment = computeVersionSegment.call(this, src.component, src.version)
166
173
  file.out = computeOut(src, family, versionSegment, this.htmlUrlExtensionStyle)
167
174
  }
168
175
  if (!file.pub && (publishable || family === 'nav')) {
169
- if (versionSegment == null) versionSegment = computeVersionSegment.bind(this)(src.component, src.version)
176
+ if (versionSegment == null) versionSegment = computeVersionSegment.call(this, src.component, src.version)
170
177
  file.pub = computePub(src, file.out, family, versionSegment, this.htmlUrlExtensionStyle)
171
178
  }
172
- this[$files].set(key, file)
179
+ filesForFamily.set(key, file)
173
180
  return file
174
181
  }
175
182
 
176
183
  findBy (criteria) {
177
184
  const criteriaEntries = Object.entries(criteria)
185
+ const family = criteria.family
186
+ if (criteriaEntries.length === 1 && family) {
187
+ const filesForFamily = this[$files].get(family)
188
+ return filesForFamily ? [...filesForFamily.values()] : []
189
+ }
178
190
  const accum = []
179
- for (const candidate of this[$files].values()) {
180
- const candidateSrc = candidate.src
181
- if (criteriaEntries.every(([key, val]) => candidateSrc[key] === val)) accum.push(candidate)
191
+ for (const filesForFamily of this[$files].values()) {
192
+ for (const candidate of filesForFamily.values()) {
193
+ const candidateSrc = candidate.src
194
+ if (criteriaEntries.every(([key, val]) => candidateSrc[key] === val)) accum.push(candidate)
195
+ }
182
196
  }
183
197
  return accum
184
198
  }
185
199
 
186
- getById ({ component, version, module: module_, family, relative }) {
187
- return this[$files].get(generateKey({ component, version, module: module_, family, relative }))
200
+ getById (id) {
201
+ return (this[$files].get(id.family) || { get: invariably.void }).get(generateKey(id))
188
202
  }
189
203
 
190
204
  getByPath ({ component, version, path: path_ }) {
191
- for (const candidate of this[$files].values()) {
192
- if (candidate.path === path_ && candidate.src.component === component && candidate.src.version === version) {
193
- return candidate
205
+ for (const filesForFamily of this[$files].values()) {
206
+ for (const it of filesForFamily.values()) {
207
+ if (it.path === path_ && it.src.component === component && it.src.version === version) return it
194
208
  }
195
209
  }
196
210
  }
@@ -214,26 +228,28 @@ class ContentCatalog {
214
228
  }
215
229
 
216
230
  getFiles () {
217
- return [...this[$files].values()]
231
+ const accum = []
232
+ for (const filesForFamily of this[$files].values()) {
233
+ for (const file of filesForFamily.values()) accum.push(file)
234
+ }
235
+ return accum
218
236
  }
219
237
 
220
238
  getPages (filter) {
221
- const accum = []
239
+ const candidates = this[$files].get('page')
240
+ if (!candidates) return []
222
241
  if (filter) {
223
- for (const candidate of this[$files].values()) {
224
- if (candidate.src.family === 'page' && filter(candidate)) accum.push(candidate)
225
- }
242
+ const accum = []
243
+ for (const candidate of candidates.values()) filter(candidate) && accum.push(candidate)
244
+ return accum
226
245
  } else {
227
- for (const candidate of this[$files].values()) {
228
- if (candidate.src.family === 'page') accum.push(candidate)
229
- }
246
+ return [...candidates.values()]
230
247
  }
231
- return accum
232
248
  }
233
249
 
234
250
  // TODO add `follow` argument to control whether alias is followed
235
251
  getSiteStartPage () {
236
- return this.getById(START_PAGE_ID) || (this.getById(START_ALIAS_ID) || {}).rel
252
+ return this.getById(ROOT_INDEX_PAGE_ID) || (this.getById(ROOT_INDEX_ALIAS_ID) || {}).rel
237
253
  }
238
254
 
239
255
  registerComponentVersionStartPage (name, componentVersion, startPageSpec = undefined) {
@@ -245,7 +261,7 @@ class ContentCatalog {
245
261
  }
246
262
  let startPage
247
263
  let startPageSrc
248
- const indexPageId = { component: name, version, module: 'ROOT', family: 'page', relative: 'index.adoc' }
264
+ const indexPageId = Object.assign({}, ROOT_INDEX_PAGE_ID, { component: name, version })
249
265
  if (startPageSpec) {
250
266
  if (
251
267
  (startPage = this.resolvePage(startPageSpec, indexPageId)) &&
@@ -273,7 +289,7 @@ class ContentCatalog {
273
289
  componentVersion.url = startPage.pub.url
274
290
  } else {
275
291
  // QUESTION: should we warn if the default start page cannot be found?
276
- const versionSegment = computeVersionSegment.bind(this)(name, version)
292
+ const versionSegment = computeVersionSegment.call(this, name, version)
277
293
  componentVersion.url = computePub(
278
294
  (startPageSrc = prepareSrc(Object.assign({}, indexPageId, { family: 'page' }))),
279
295
  computeOut(startPageSrc, startPageSrc.family, versionSegment, this.htmlUrlExtensionStyle),
@@ -286,7 +302,7 @@ class ContentCatalog {
286
302
  const symbolicVersionAlias = createSymbolicVersionAlias(
287
303
  name,
288
304
  version,
289
- computeVersionSegment.bind(this)(name, version, 'alias'),
305
+ computeVersionSegment.call(this, name, version, 'alias'),
290
306
  this.latestVersionUrlSegmentStrategy
291
307
  )
292
308
  if (symbolicVersionAlias) this.addFile(symbolicVersionAlias)
@@ -296,7 +312,8 @@ class ContentCatalog {
296
312
  if (!startPageSpec) return
297
313
  const rel = this.resolvePage(startPageSpec)
298
314
  if (rel) {
299
- return this.addFile({ src: Object.assign({}, START_ALIAS_ID), rel })
315
+ if (!(this.getSiteStartPage() || { synthetic: true }).synthetic) return
316
+ return this.addFile({ src: Object.assign({}, ROOT_INDEX_ALIAS_ID), rel, synthetic: true })
300
317
  } else if (rel === false) {
301
318
  logger.warn('Start page specified for site has invalid syntax: %s', startPageSpec)
302
319
  } else if (~startPageSpec.indexOf(':')) {
@@ -328,18 +345,17 @@ class ContentCatalog {
328
345
  ` existing page: ${getFileLocation(existingPage)}`
329
346
  )
330
347
  }
331
- const existingAlias = this.getById(Object.assign({}, src, { family: 'alias' }))
332
- if (existingAlias) {
333
- throw new Error(
334
- `Duplicate alias: ${generateResourceSpec(src)} (specified as: ${spec})\n` +
335
- ` source: ${getFileLocation(target)}`
336
- )
337
- }
338
348
  } else if (src.version == null) {
339
349
  // QUESTION should we skip registering alias in this case?
340
350
  src.version = ''
341
351
  }
342
352
  src.family = 'alias'
353
+ const existingAlias = this.getById(src)
354
+ if (existingAlias) {
355
+ throw new Error(
356
+ `Duplicate alias: ${generateResourceSpec(src)} (specified as: ${spec})\n source: ${getFileLocation(target)}`
357
+ )
358
+ }
343
359
  // NOTE the redirect producer will populate contents when the redirect facility is 'static'
344
360
  const alias = this.addFile({ src, rel: target })
345
361
  // NOTE record the first alias this target claims as the preferred one
@@ -352,9 +368,10 @@ class ContentCatalog {
352
368
  *
353
369
  * Parses the specified contextual page ID spec into a page ID object, then attempts to lookup a
354
370
  * file with this page ID in the catalog. If a component is specified, but not a version, the
355
- * latest version of the component stored in the catalog is used. If a file cannot be resolved,
356
- * the function returns undefined. If the spec does not match the page ID syntax, this function
357
- * throws an error.
371
+ * latest version of the component stored in the catalog is used. If a page cannot be resolved,
372
+ * the search is attempted again for an "alias". If neither a page or alias can be resolved, the
373
+ * function returns undefined. If the spec does not match the page ID syntax, this function throws
374
+ * an error.
358
375
  *
359
376
  * @param {String} spec - The contextual page ID spec (e.g., version@component:module:topic/page.adoc).
360
377
  * @param {ContentCatalog} catalog - The content catalog in which to resolve the page file.
@@ -394,8 +411,8 @@ class ContentCatalog {
394
411
  */
395
412
  ContentCatalog.prototype.getAll = ContentCatalog.prototype.getFiles
396
413
 
397
- function generateKey ({ component, version, module: module_, family, relative }) {
398
- return `${version}@${component}:${module_}:${family}$${relative}`
414
+ function generateKey ({ component, version, module: module_, relative }) {
415
+ return `${version}@${component}:${module_}:${relative}`
399
416
  }
400
417
 
401
418
  function generateResourceSpec ({ component, version, module: module_, family, relative }, shorthand = true) {
@@ -435,6 +452,7 @@ function prepareSrc (src) {
435
452
 
436
453
  function computeOut (src, family, version, htmlUrlExtensionStyle) {
437
454
  let { component, module: module_, basename, extname, relative, stem } = src
455
+ if (component === 'ROOT') component = ''
438
456
  if (module_ === 'ROOT') module_ = ''
439
457
  let indexifyPathSegment = ''
440
458
  let familyPathSegment = ''
@@ -465,8 +483,11 @@ function computePub (src, out, family, version, htmlUrlExtensionStyle) {
465
483
  const pub = {}
466
484
  let url
467
485
  if (family === 'nav') {
468
- const urlSegments = version ? [src.component, version] : [src.component]
469
- if (src.module && src.module !== 'ROOT') urlSegments.push(src.module)
486
+ const component = src.component || 'ROOT'
487
+ const urlSegments = component === 'ROOT' ? [] : [component]
488
+ if (version) urlSegments.push(version)
489
+ const module_ = src.module || 'ROOT'
490
+ if (module_ !== 'ROOT') urlSegments.push(module_)
470
491
  // an artificial URL used for resolving page references in navigation model
471
492
  url = '/' + urlSegments.join('/') + '/'
472
493
  pub.moduleRootPath = '.'
@@ -56,9 +56,16 @@ function parseResourceId (spec, ctx = {}, defaultFamily = 'page', permittedFamil
56
56
  }
57
57
 
58
58
  if (~relative.indexOf('/')) {
59
- relative = relative
60
- .split('/')
61
- .filter((it) => it && it !== '.' && it !== '..')
59
+ const relativeSegments = relative.split('/')
60
+ let from
61
+ if (relativeSegments[0] === '.' && (from = ctx.relative)) {
62
+ relativeSegments[0] = from.substr(0, (from.lastIndexOf('/') + 1 || 1) - 1)
63
+ }
64
+ relative = relativeSegments
65
+ .reduce((accum, segment) => {
66
+ segment === '..' ? accum.pop() : (segment || '.') !== '.' && accum.push(segment)
67
+ return accum
68
+ }, [])
62
69
  .join('/')
63
70
  }
64
71
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antora/content-classifier",
3
- "version": "3.0.0-alpha.7",
3
+ "version": "3.0.0-beta.2",
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)",
@@ -16,12 +16,12 @@
16
16
  },
17
17
  "main": "lib/index.js",
18
18
  "dependencies": {
19
- "@antora/logger": "3.0.0-alpha.7",
19
+ "@antora/logger": "3.0.0-beta.2",
20
20
  "mime-types": "~2.1",
21
21
  "vinyl": "~2.2"
22
22
  },
23
23
  "engines": {
24
- "node": ">=10.17.0"
24
+ "node": ">=12.21.0"
25
25
  },
26
26
  "files": [
27
27
  "lib/"
@@ -34,5 +34,5 @@
34
34
  "static site",
35
35
  "web publishing"
36
36
  ],
37
- "gitHead": "fbd597b3680474f2083cda8a7facf1e2848c08e0"
37
+ "gitHead": "5cd3f9cc70622e465cb44daf1aa2035ed5a35f54"
38
38
  }