@antora/content-classifier 3.2.0-alpha.1 → 3.2.0-alpha.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.
- package/lib/classify-content.js +96 -47
- package/lib/content-catalog.js +135 -89
- package/lib/util/resolve-resource.js +1 -3
- package/lib/util/summarize-file-location.js +14 -0
- package/lib/util/version-compare-desc.js +11 -19
- package/package.json +12 -8
package/lib/classify-content.js
CHANGED
|
@@ -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
|
-
* @
|
|
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
|
|
22
|
-
aggregate
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
49
|
-
const
|
|
50
|
-
if (
|
|
51
|
-
|
|
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 (
|
|
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 (
|
|
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
|
|
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 =
|
|
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)
|
|
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
|
}
|
package/lib/content-catalog.js
CHANGED
|
@@ -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:
|
|
62
|
-
const componentVersion = {
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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 (
|
|
138
|
+
if (startPageRef) {
|
|
131
139
|
// @deprecated use separate call to register start page for component version
|
|
132
|
-
this.registerComponentVersionStartPage(name, componentVersion,
|
|
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
|
|
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
|
|
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' &&
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 ||
|
|
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
|
-
|
|
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
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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
|
-
|
|
354
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
412
|
+
` (specified as: ${spec})${LOG_WRAP}source: ${summarizeFileLocation(existingPage)}`
|
|
388
413
|
: `Page alias cannot reference an existing page: ${generateResourceSpec(src)} (specified as: ${spec})` +
|
|
389
|
-
|
|
390
|
-
|
|
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: ${
|
|
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
|
|
460
|
+
* Attempts to resolve a page reference within the given context to a page in the catalog.
|
|
436
461
|
*
|
|
437
|
-
* Parses the specified
|
|
438
|
-
*
|
|
439
|
-
*
|
|
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
|
-
*
|
|
445
|
-
*
|
|
446
|
-
*
|
|
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
|
-
* @
|
|
449
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
516
|
-
extname = basename.substr(stem.length)
|
|
559
|
+
src.extname = basename.substr(stem.length)
|
|
517
560
|
}
|
|
518
|
-
return
|
|
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.
|
|
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
|
|
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
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
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' ?
|
|
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
|
-
|
|
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]
|
|
68
|
-
const numB = Number(numsB[i]
|
|
69
|
-
if (numA > numB)
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
return -1
|
|
73
|
-
} else if (isNaN(
|
|
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
|
-
|
|
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.
|
|
3
|
+
"version": "3.2.0-alpha.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)",
|
|
@@ -10,7 +10,11 @@
|
|
|
10
10
|
"Hubert SABLONNIÈRE <hubert.sablonniere@gmail.com>"
|
|
11
11
|
],
|
|
12
12
|
"homepage": "https://antora.org",
|
|
13
|
-
"repository":
|
|
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.
|
|
31
|
-
"@antora/logger": "3.2.0-alpha.
|
|
34
|
+
"@antora/asciidoc-loader": "3.2.0-alpha.11",
|
|
35
|
+
"@antora/logger": "3.2.0-alpha.11",
|
|
32
36
|
"mime-types": "~2.1",
|
|
33
|
-
"vinyl": "~
|
|
37
|
+
"vinyl": "~3.0"
|
|
34
38
|
},
|
|
35
39
|
"engines": {
|
|
36
|
-
"node": ">=
|
|
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": "
|
|
52
|
-
"postpublish": "
|
|
55
|
+
"prepublishOnly": "npx -y downdoc@latest --prepublish",
|
|
56
|
+
"postpublish": "npx -y downdoc@latest --postpublish"
|
|
53
57
|
}
|
|
54
58
|
}
|