@antora/assembler 1.0.0-alpha.6 → 1.0.0-alpha.8
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/assemble-content.js +3 -2
- package/lib/filter-component-versions.js +2 -2
- package/lib/load-config.js +10 -5
- package/lib/produce-aggregate-document.js +157 -82
- package/lib/produce-aggregate-documents.js +64 -17
- package/lib/util/lazy-readable.js +1 -1
- package/lib/util/run-command.js +2 -2
- package/lib/util/sanitize.js +15 -0
- package/package.json +6 -6
package/lib/assemble-content.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const loadConfig = require('./load-config')
|
|
4
4
|
const produceAggregateDocuments = require('./produce-aggregate-documents')
|
|
5
5
|
const PromiseQueue = require('./util/promise-queue')
|
|
6
|
+
const runCommand = require('./util/run-command')
|
|
6
7
|
|
|
7
8
|
async function assembleContent (playbook, contentCatalog, converter, { siteCatalog, configSource }) {
|
|
8
9
|
// Q: could we get ContentCatalog#getComponentVersionStartPage() in Antora core?
|
|
@@ -23,7 +24,7 @@ async function assembleContent (playbook, contentCatalog, converter, { siteCatal
|
|
|
23
24
|
// TODO: pass more information to converter so it doesn't have to compute internal stuff
|
|
24
25
|
// Q: don't we need to pass in the combined/resolved AsciiDoc attributes per file or component version?
|
|
25
26
|
return new PromiseQueue({ concurrency: buildConfig.processLimit })
|
|
26
|
-
.add(aggregateDocuments.map((doc) => () => converter.call(this, doc, buildConfig)))
|
|
27
|
+
.add(aggregateDocuments.map((doc) => () => converter.call(this, doc, buildConfig, runCommand)))
|
|
27
28
|
.toPromise()
|
|
28
29
|
.then((files) => {
|
|
29
30
|
if (buildConfig.publish && siteCatalog) siteCatalog.addFiles(files)
|
|
@@ -31,7 +32,7 @@ async function assembleContent (playbook, contentCatalog, converter, { siteCatal
|
|
|
31
32
|
})
|
|
32
33
|
}
|
|
33
34
|
|
|
34
|
-
// TODO: if no workspace dir is defined
|
|
35
|
+
// TODO: if no workspace dir is defined, we shouldn't continue
|
|
35
36
|
function prepareWorkspace (publishFiles, aggregateDocuments, contentCatalog, buildConfig) {
|
|
36
37
|
const { dir, clean, keepAggregateSource } = buildConfig
|
|
37
38
|
const files = contentCatalog.findBy({ family: 'image' }).filter(({ out }) => out?.assembled)
|
|
@@ -21,11 +21,11 @@ function compilePatterns (patterns) {
|
|
|
21
21
|
if (patterns[0].charAt() === '!') patterns = ['**', ...patterns]
|
|
22
22
|
return patterns.map((pattern) => {
|
|
23
23
|
const negated = pattern.charAt() === '!'
|
|
24
|
-
if (negated) pattern = pattern.
|
|
24
|
+
if (negated) pattern = pattern.slice(1)
|
|
25
25
|
let version
|
|
26
26
|
const separatorIdx = pattern.search(VERSION_SEPARATOR_RX)
|
|
27
27
|
if (~separatorIdx) {
|
|
28
|
-
pattern = (version = true) && `${pattern.
|
|
28
|
+
pattern = (version = true) && `${pattern.slice(0, separatorIdx)}%${pattern.slice(separatorIdx + 1) || '*'}`
|
|
29
29
|
}
|
|
30
30
|
return Object.assign(makePicomatchRx(pattern, PICOMATCH_OPTS), {
|
|
31
31
|
globstar: pattern === '**',
|
package/lib/load-config.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const expandPath = require('@antora/expand-path-helper')
|
|
4
|
-
const
|
|
5
|
-
const os = require('os')
|
|
4
|
+
const fsp = require('node:fs/promises')
|
|
5
|
+
const os = require('node:os')
|
|
6
6
|
const yaml = require('js-yaml')
|
|
7
7
|
|
|
8
8
|
function loadConfig (playbook, configSource = './antora-assembler.yml') {
|
|
@@ -27,8 +27,9 @@ function loadConfig (playbook, configSource = './antora-assembler.yml') {
|
|
|
27
27
|
config.asciidoc.attributes = asciidocAttrs = {}
|
|
28
28
|
}
|
|
29
29
|
if (!('doctype' in asciidocAttrs)) asciidocAttrs.doctype = 'book'
|
|
30
|
-
asciidocAttrs.revdate =
|
|
30
|
+
if (!('revdate' in asciidocAttrs)) asciidocAttrs.revdate = getLocalDate()
|
|
31
31
|
asciidocAttrs['page-partial'] = null
|
|
32
|
+
asciidocAttrs['loader-assembler'] = ''
|
|
32
33
|
if (config.componentVersions == null) {
|
|
33
34
|
config.componentVersions = ['*']
|
|
34
35
|
} else if (typeof config.componentVersions === 'string') {
|
|
@@ -46,7 +47,7 @@ function loadConfig (playbook, configSource = './antora-assembler.yml') {
|
|
|
46
47
|
} else {
|
|
47
48
|
build.dir = expandPath(build.dir || './build/assembler', { dot: playbook.dir })
|
|
48
49
|
}
|
|
49
|
-
build.cwd = playbook.dir // use playbook.dir the purpose of finding and loading require scripts
|
|
50
|
+
build.cwd = playbook.dir // use playbook.dir for the purpose of finding and loading require scripts
|
|
50
51
|
if (!('clean' in build) && 'output' in playbook) build.clean = playbook.output.clean
|
|
51
52
|
if (!('publish' in build)) build.publish = true
|
|
52
53
|
if (!build.processLimit) {
|
|
@@ -62,10 +63,14 @@ function camelCaseKeys (o, stopPaths = [], p) {
|
|
|
62
63
|
const pathPrefix = p ? p + '.' : ''
|
|
63
64
|
const accum = {}
|
|
64
65
|
for (const [k, v] of Object.entries(o)) {
|
|
65
|
-
const camelKey = k.charAt() + k.
|
|
66
|
+
const camelKey = k.charAt() + k.slice(1).replace(/_([a-z])/g, (_, l) => l.toUpperCase())
|
|
66
67
|
accum[camelKey] = ~stopPaths.indexOf(pathPrefix + camelKey) ? v : camelCaseKeys(v, stopPaths, pathPrefix + camelKey)
|
|
67
68
|
}
|
|
68
69
|
return accum
|
|
69
70
|
}
|
|
70
71
|
|
|
72
|
+
function getLocalDate (now = new Date()) {
|
|
73
|
+
return new Date(now - now.getTimezoneOffset() * 60000).toISOString().split('T')[0]
|
|
74
|
+
}
|
|
75
|
+
|
|
71
76
|
module.exports = loadConfig
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const File = require('vinyl')
|
|
4
|
-
const
|
|
4
|
+
const path = require('node:path/posix')
|
|
5
|
+
const sanitize = require('./util/sanitize')
|
|
5
6
|
|
|
6
7
|
function produceAggregateDocument (
|
|
7
8
|
loadAsciiDoc,
|
|
@@ -16,7 +17,17 @@ function produceAggregateDocument (
|
|
|
16
17
|
) {
|
|
17
18
|
const pagesInOutline = selectPagesInOutline(outline, pages)
|
|
18
19
|
const navtitle = outline.content
|
|
19
|
-
const
|
|
20
|
+
const templateFile = contentCatalog.addFile({
|
|
21
|
+
src: {
|
|
22
|
+
component: componentVersion.name,
|
|
23
|
+
version: componentVersion.version,
|
|
24
|
+
module: 'ROOT',
|
|
25
|
+
family: 'page',
|
|
26
|
+
relative: generateSlug(navtitle),
|
|
27
|
+
},
|
|
28
|
+
})
|
|
29
|
+
const path_ = `${templateFile.out.path}.adoc`
|
|
30
|
+
contentCatalog.removeFile(templateFile)
|
|
20
31
|
const header = buildAsciiDocHeader(componentVersion, navtitle, doctype)
|
|
21
32
|
const body = aggregateAsciiDoc(
|
|
22
33
|
loadAsciiDoc,
|
|
@@ -29,29 +40,32 @@ function produceAggregateDocument (
|
|
|
29
40
|
mutableAttributes,
|
|
30
41
|
sectionMergeStrategy
|
|
31
42
|
)
|
|
32
|
-
const relativeSrcPath = `${stem}.adoc`
|
|
33
43
|
return new File({
|
|
44
|
+
aggregate: true,
|
|
34
45
|
asciidoc: asciidocConfig,
|
|
35
46
|
contents: Buffer.from([...header, ...body].join('\n') + '\n'),
|
|
36
47
|
mediaType: 'text/asciidoc',
|
|
37
|
-
path:
|
|
48
|
+
path: path_,
|
|
38
49
|
src: {
|
|
39
50
|
component: componentVersion.name,
|
|
40
51
|
version: componentVersion.version,
|
|
41
|
-
basename: path.basename(
|
|
42
|
-
stem,
|
|
52
|
+
basename: path.basename(path_),
|
|
53
|
+
stem: path.basename(path_, '.adoc'),
|
|
43
54
|
extname: '.adoc',
|
|
44
55
|
},
|
|
45
56
|
})
|
|
46
57
|
}
|
|
47
58
|
|
|
48
59
|
function buildAsciiDocHeader (componentVersion, navtitle, doctype = 'book') {
|
|
49
|
-
const
|
|
60
|
+
const [navtitlePlain, navtitleAsciiDoc] = sanitize(navtitle)
|
|
61
|
+
let doctitle = navtitleAsciiDoc
|
|
62
|
+
if (navtitlePlain !== componentVersion.title) doctitle = `${componentVersion.title}: ${doctitle}`
|
|
50
63
|
const version = componentVersion.version === 'master' ? '' : componentVersion.version
|
|
51
64
|
return [
|
|
52
65
|
`= ${doctitle}`,
|
|
53
|
-
...(version ? [
|
|
66
|
+
...(version ? [`:revnumber: ${version}`] : []),
|
|
54
67
|
...(doctype === 'article' ? [] : [`:doctype: ${doctype}`]),
|
|
68
|
+
':underscore: _',
|
|
55
69
|
// Q: should we pass these via the CLI so they cannot be modified?
|
|
56
70
|
`:page-component-name: ${componentVersion.name}`,
|
|
57
71
|
`:page-component-version:${version ? ' ' + version : ''}`,
|
|
@@ -84,16 +98,24 @@ function aggregateAsciiDoc (
|
|
|
84
98
|
asciidocConfig,
|
|
85
99
|
mutableAttributes,
|
|
86
100
|
sectionMergeStrategy,
|
|
101
|
+
lastComponentVersion = componentVersion,
|
|
87
102
|
level = 0
|
|
88
103
|
) {
|
|
89
104
|
const buffer = []
|
|
90
105
|
// TODO: we could try to be smart about it and make sure the page with fragment is included at least once
|
|
91
106
|
if (outlineEntry.hash) return buffer
|
|
92
107
|
const { content: navtitle, items, unresolved, urlType, url } = outlineEntry
|
|
108
|
+
const [navtitlePlain, navtitleAsciiDoc, navtitlePass] = sanitize(navtitle)
|
|
109
|
+
const siteUrl = ((val) => {
|
|
110
|
+
if (!val || val === '/') return ''
|
|
111
|
+
return val.charAt(val.length - 1) === '/' ? val.slice(0, val.length - 1) : val
|
|
112
|
+
})(asciidocConfig.attributes['site-url'])
|
|
93
113
|
// FIXME: ideally, resource ID would be stored in navigation so we can look up the page more efficiently
|
|
94
114
|
let page = urlType === 'internal' && !unresolved ? pagesInOutline.get(url) : undefined
|
|
115
|
+
if (page && pagesInOutline.aggregated?.includes(page)) page = undefined
|
|
95
116
|
if (page) {
|
|
96
117
|
let contents = page.src.contents
|
|
118
|
+
if (contents == null) return buffer
|
|
97
119
|
// NOTE: blank lines at top and bottom of document create mismatch when using line numbers to navigate source lines
|
|
98
120
|
// IMPORTANT: this must not leave behind lines the parser will drop!
|
|
99
121
|
// IDEA: another option is to capture initial lineno of reader and use as offset (but preseves those blank lines)
|
|
@@ -103,17 +125,36 @@ function aggregateAsciiDoc (
|
|
|
103
125
|
.replace(/^(?:[ \t]*\r\n?|[ \t]*\n)+/, '')
|
|
104
126
|
.trimRight()
|
|
105
127
|
)
|
|
128
|
+
;(pagesInOutline.aggregated ??= []).push(page)
|
|
106
129
|
page = new page.constructor(Object.assign({}, page, { contents, mediaType: 'text/asciidoc' }))
|
|
107
|
-
const { module: module_, relative, origin } = page.src
|
|
130
|
+
const { component, version, module: module_, relative, origin } = page.src
|
|
108
131
|
const doc = loadAsciiDoc(page, contentCatalog, asciidocConfig)
|
|
109
132
|
const refs = doc.getCatalog().refs
|
|
110
133
|
// NOTE: in Antora, docname is relative src path from module without file extension
|
|
111
134
|
const docname = doc.getAttribute('docname')
|
|
112
|
-
|
|
113
|
-
const
|
|
114
|
-
|
|
135
|
+
const docnameForId = docname.replace(/[/]/g, '::').replace(/[.]/g, '-')
|
|
136
|
+
const scopeId = component !== componentVersion.name
|
|
137
|
+
const idprefix =
|
|
138
|
+
(scopeId ? component + ':' : '') +
|
|
139
|
+
(module_ === 'ROOT' ? (scopeId ? ':' : '') : module_ + ':') +
|
|
140
|
+
docnameForId +
|
|
141
|
+
':::'
|
|
115
142
|
buffer.push('')
|
|
116
143
|
buffer.push(`:docname: ${docname}`)
|
|
144
|
+
if (component !== lastComponentVersion.name) {
|
|
145
|
+
const thisComponentVersion =
|
|
146
|
+
component === componentVersion.name && version === componentVersion.version
|
|
147
|
+
? componentVersion
|
|
148
|
+
: contentCatalog.getComponentVersion(component, version)
|
|
149
|
+
if (thisComponentVersion) {
|
|
150
|
+
buffer.push(`:page-component-name: ${thisComponentVersion.name}`)
|
|
151
|
+
buffer.push(`:page-component-version:${thisComponentVersion.version ? ' ' + thisComponentVersion.version : ''}`)
|
|
152
|
+
buffer.push(':page-version: {page-component-version}')
|
|
153
|
+
buffer.push(`:page-component-display-version: ${thisComponentVersion.displayVersion}`)
|
|
154
|
+
buffer.push(`:page-component-title: ${thisComponentVersion.title}`)
|
|
155
|
+
lastComponentVersion = thisComponentVersion
|
|
156
|
+
}
|
|
157
|
+
}
|
|
117
158
|
buffer.push(`:page-module: ${module_}`)
|
|
118
159
|
buffer.push(`:page-relative-src-path: ${relative}`)
|
|
119
160
|
//buffer.push(`:page-origin-type: ${origin.type}`)
|
|
@@ -125,7 +166,7 @@ function aggregateAsciiDoc (
|
|
|
125
166
|
let enclosed
|
|
126
167
|
// NOTE: if level is 0, doctitle has already been added and we're in the document header
|
|
127
168
|
if (level) {
|
|
128
|
-
if (level === 1 &&
|
|
169
|
+
if (level === 1 && navtitlePlain === componentVersion.title) {
|
|
129
170
|
level--
|
|
130
171
|
} else {
|
|
131
172
|
let hlevel = level + 1
|
|
@@ -135,7 +176,7 @@ function aggregateAsciiDoc (
|
|
|
135
176
|
} else {
|
|
136
177
|
buffer.push(`[#${idprefix}]`)
|
|
137
178
|
}
|
|
138
|
-
buffer.push(`${'='.repeat(hlevel)} ${
|
|
179
|
+
buffer.push(`${'='.repeat(hlevel)} ${navtitleAsciiDoc}`)
|
|
139
180
|
}
|
|
140
181
|
} else {
|
|
141
182
|
header.unshift(`[#${idprefix}]`)
|
|
@@ -167,10 +208,6 @@ function aggregateAsciiDoc (
|
|
|
167
208
|
buffer.push(`${'='.repeat(hlevel)} ${overviewTitle}`)
|
|
168
209
|
if (toggleSectids) buffer.push(':sectids:')
|
|
169
210
|
}
|
|
170
|
-
const siteUrl = ((val) => {
|
|
171
|
-
if (!val || val === '/') return ''
|
|
172
|
-
return val.charAt(val.length - 1) === '/' ? val.substr(0, val.length - 1) : val
|
|
173
|
-
})(doc.getAttribute('site-url'))
|
|
174
211
|
const lines = doc.getSourceLines()
|
|
175
212
|
const ignoreLines = []
|
|
176
213
|
// TODO: think more about when multipart is allowed; perhaps configurable
|
|
@@ -206,9 +243,23 @@ function aggregateAsciiDoc (
|
|
|
206
243
|
for (let i = idx; i < block.lines.length + (delimited ? idx + 2 : idx); i++) ignoreLines.push(i)
|
|
207
244
|
}
|
|
208
245
|
})
|
|
246
|
+
let skipping
|
|
209
247
|
for (let idx = 0, len = lines.length; idx < len; idx++) {
|
|
210
248
|
if (~ignoreLines.indexOf(idx)) continue
|
|
211
249
|
let line = lines[idx]
|
|
250
|
+
if (line.startsWith('//')) {
|
|
251
|
+
if (line[2] !== '/') continue
|
|
252
|
+
if (line.length > 3 && line === '/'.repeat(line.length)) {
|
|
253
|
+
if (skipping) {
|
|
254
|
+
if (line === skipping) skipping = undefined
|
|
255
|
+
} else {
|
|
256
|
+
skipping = line
|
|
257
|
+
}
|
|
258
|
+
continue
|
|
259
|
+
}
|
|
260
|
+
} else if (skipping) {
|
|
261
|
+
continue
|
|
262
|
+
}
|
|
212
263
|
if (line.charAt() === ':' && /^:(?:leveloffset: .*|!leveloffset:|leveloffset!:)$/.test(line)) {
|
|
213
264
|
if (lines[idx - 1] === '') lines[idx - 1] = undefined
|
|
214
265
|
lines[idx] = undefined
|
|
@@ -233,8 +284,8 @@ function aggregateAsciiDoc (
|
|
|
233
284
|
let pagePart, fragment, targetPage
|
|
234
285
|
const hashIdx = target.indexOf('#')
|
|
235
286
|
if (~hashIdx) {
|
|
236
|
-
pagePart = target.
|
|
237
|
-
fragment = target.
|
|
287
|
+
pagePart = target.slice(0, hashIdx)
|
|
288
|
+
fragment = target.slice(hashIdx + 1)
|
|
238
289
|
// TODO: for now, assume .adoc; in the future, consider other file extensions
|
|
239
290
|
if (pagePart && !pagePart.endsWith('.adoc')) pagePart += '.adoc'
|
|
240
291
|
} else if (target.endsWith('.adoc')) {
|
|
@@ -250,22 +301,29 @@ function aggregateAsciiDoc (
|
|
|
250
301
|
: `<<${idprefix}${fragment}${text ? ',' + text.replace(/\\]/g, ']') : ''}>>`
|
|
251
302
|
}
|
|
252
303
|
if (~pagePart.indexOf('@') || /:.*:/.test(pagePart)) {
|
|
304
|
+
if (siteUrl && (targetPage = contentCatalog.resolvePage(pagePart, page.src)) && targetPage.out) {
|
|
305
|
+
text ||= targetPage.asciidoc?.xreftext || target
|
|
306
|
+
return `${siteUrl}${targetPage.pub.url}${fragment && '#' + fragment}[${text}]`
|
|
307
|
+
}
|
|
253
308
|
// TODO: handle unresolved page better
|
|
254
|
-
return
|
|
255
|
-
? `${siteUrl}${targetPage.pub.url}${fragment && '#' + fragment}[${text}]`
|
|
256
|
-
: m
|
|
309
|
+
return m
|
|
257
310
|
} else if (pagePart.indexOf(':') < 0) {
|
|
258
311
|
if (module_ !== 'ROOT') pagePart = `${module_}:${pagePart}`
|
|
259
312
|
} else if (pagePart.startsWith('ROOT:')) {
|
|
260
|
-
pagePart = pagePart.
|
|
313
|
+
pagePart = pagePart.slice(5)
|
|
261
314
|
}
|
|
262
315
|
if (!(targetPage = pagesInOutline.get(pagePart))) {
|
|
316
|
+
if (siteUrl && (targetPage = contentCatalog.resolvePage(pagePart, page.src)) && targetPage.out) {
|
|
317
|
+
text ||= targetPage.asciidoc?.xreftext || target
|
|
318
|
+
return `${siteUrl}${targetPage.pub.url}${fragment && '#' + fragment}[${text}]`
|
|
319
|
+
}
|
|
263
320
|
// TODO: handle unresolved page better
|
|
264
|
-
return
|
|
265
|
-
? `${siteUrl}${targetPage.pub.url}${fragment && '#' + fragment}[${text}]`
|
|
266
|
-
: m
|
|
321
|
+
return m
|
|
267
322
|
}
|
|
268
|
-
pagePart = pagePart
|
|
323
|
+
pagePart = pagePart
|
|
324
|
+
.replace(/[/]/g, '::')
|
|
325
|
+
.replace(/\.adoc$/, '')
|
|
326
|
+
.replace(/[.]/g, '-')
|
|
269
327
|
const refid = `${pagePart}:::${fragment}`
|
|
270
328
|
return `<<${refid}${text && text !== targetPage.title ? ',' + text.replace(/\\]/g, ']') : ''}>>`
|
|
271
329
|
})
|
|
@@ -281,7 +339,10 @@ function aggregateAsciiDoc (
|
|
|
281
339
|
family: 'attachment',
|
|
282
340
|
relative,
|
|
283
341
|
})
|
|
284
|
-
|
|
342
|
+
// TODO: handle unresolved attachment page
|
|
343
|
+
return attachment && attachment.out
|
|
344
|
+
? `${siteUrl}${attachment.pub.url.replace(/_/g, '{underscore}')}[${text}]`
|
|
345
|
+
: m
|
|
285
346
|
})
|
|
286
347
|
}
|
|
287
348
|
if (~line.indexOf('image:') && !line.startsWith('image::')) {
|
|
@@ -289,9 +350,9 @@ function aggregateAsciiDoc (
|
|
|
289
350
|
if (isResourceSpec(target)) {
|
|
290
351
|
const image = contentCatalog.resolveResource(target, page.src, 'image', ['image'])
|
|
291
352
|
// TODO: handle (or report) unresolved image better
|
|
292
|
-
if (image) {
|
|
353
|
+
if (image && image.out) {
|
|
293
354
|
image.out.assembled = true
|
|
294
|
-
return `image:${image.
|
|
355
|
+
return `image:${image.out.path.replace(/_/g, '{underscore}')}[${attrlist}]`
|
|
295
356
|
}
|
|
296
357
|
}
|
|
297
358
|
return m
|
|
@@ -331,14 +392,14 @@ function aggregateAsciiDoc (
|
|
|
331
392
|
let prefix = ''
|
|
332
393
|
// Q: can we use startsWith('image::') in certain cases?
|
|
333
394
|
let imageMacroOffset = (
|
|
334
|
-
lastImageMacroAt?.[0] === idx ? line.
|
|
395
|
+
lastImageMacroAt?.[0] === idx ? line.slice(0, lastImageMacroAt[1]) : line
|
|
335
396
|
).lastIndexOf('image::')
|
|
336
397
|
if (imageMacroOffset > 0) {
|
|
337
398
|
if (
|
|
338
399
|
block.getDocument().isNested() &&
|
|
339
|
-
(prefix = line.
|
|
400
|
+
(prefix = line.slice(0, imageMacroOffset)).trimRight().endsWith('|')
|
|
340
401
|
) {
|
|
341
|
-
line = line.
|
|
402
|
+
line = line.slice(prefix.length)
|
|
342
403
|
} else {
|
|
343
404
|
imageMacroOffset = -1
|
|
344
405
|
}
|
|
@@ -348,9 +409,10 @@ function aggregateAsciiDoc (
|
|
|
348
409
|
if (isResourceSpec(target)) {
|
|
349
410
|
const image = contentCatalog.resolveResource(target, page.src, 'image', ['image'])
|
|
350
411
|
// FIXME: handle (or report) case when image is not resolved
|
|
351
|
-
if (image) {
|
|
352
|
-
|
|
412
|
+
if (image && image.out) {
|
|
413
|
+
const boxedAttrlist = line.slice(line.indexOf('['))
|
|
353
414
|
image.out.assembled = true
|
|
415
|
+
lines[idx] = `${prefix}image::${image.out.path}${boxedAttrlist}`
|
|
354
416
|
}
|
|
355
417
|
}
|
|
356
418
|
lastImageMacroAt = [idx, imageMacroOffset]
|
|
@@ -375,7 +437,15 @@ function aggregateAsciiDoc (
|
|
|
375
437
|
} else if (val !== initialVal) {
|
|
376
438
|
accum.push(`:${name}:${initialVal ? ' ' + initialVal : ''}`)
|
|
377
439
|
}
|
|
378
|
-
} else if (
|
|
440
|
+
} else if (
|
|
441
|
+
!(
|
|
442
|
+
val == null ||
|
|
443
|
+
doc.isAttributeLocked(name) ||
|
|
444
|
+
name === 'doctype' ||
|
|
445
|
+
name === 'leveloffset' ||
|
|
446
|
+
name === 'underscore'
|
|
447
|
+
)
|
|
448
|
+
) {
|
|
379
449
|
accum.push(`:!${name}:`)
|
|
380
450
|
}
|
|
381
451
|
return accum
|
|
@@ -384,43 +454,54 @@ function aggregateAsciiDoc (
|
|
|
384
454
|
)
|
|
385
455
|
if (resolvedAttributeEntries.length > 1) buffer.push(...resolvedAttributeEntries)
|
|
386
456
|
}
|
|
387
|
-
} else {
|
|
388
|
-
if (level) {
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
457
|
+
} else if (level) {
|
|
458
|
+
if (level === 1 && navtitlePlain === componentVersion.title) {
|
|
459
|
+
level--
|
|
460
|
+
} else {
|
|
461
|
+
buffer.push('')
|
|
462
|
+
// NOTE: try to toggle sectids; otherwise, fallback to globally unique synthetic ID
|
|
463
|
+
// Q: should we unset docname, page-module, etc?
|
|
464
|
+
let toggleSectids, syntheticId
|
|
465
|
+
if (!('sectids' in asciidocConfig.attributes)) {
|
|
466
|
+
buffer.push(':!sectids:')
|
|
467
|
+
toggleSectids = true
|
|
468
|
+
} else if (typeof asciidocConfig.attributes.sectids === 'string') {
|
|
469
|
+
if ('sectids' in mutableAttributes) {
|
|
396
470
|
buffer.push(':!sectids:')
|
|
397
471
|
toggleSectids = true
|
|
398
|
-
} else
|
|
399
|
-
|
|
400
|
-
buffer.push(':!sectids:')
|
|
401
|
-
toggleSectids = true
|
|
402
|
-
} else {
|
|
403
|
-
syntheticId = `__object-id-${global.Opal.hash(outlineEntry).$object_id()}`
|
|
404
|
-
}
|
|
472
|
+
} else {
|
|
473
|
+
syntheticId = `__object-id-${global.Opal.hash(outlineEntry).$object_id()}`
|
|
405
474
|
}
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
475
|
+
}
|
|
476
|
+
let sectionTitle = navtitleAsciiDoc
|
|
477
|
+
if (urlType === 'external') {
|
|
478
|
+
sectionTitle = `${url}[${navtitlePass ? navtitleAsciiDoc : navtitleAsciiDoc.replace(/\]/g, '\\]')}]`
|
|
479
|
+
} else if (urlType === 'internal' && !unresolved && siteUrl) {
|
|
480
|
+
const resource = contentCatalog.getFiles().find((it) => it.pub.url === url)
|
|
481
|
+
if (resource?.out) {
|
|
482
|
+
sectionTitle = navtitlePass ? navtitleAsciiDoc : navtitleAsciiDoc.replace(/\]/g, '\\]')
|
|
483
|
+
sectionTitle = `${siteUrl}${resource.pub.url}[${sectionTitle}]`
|
|
414
484
|
}
|
|
415
|
-
buffer.push(`${'='.repeat(hlevel)} ${sectionTitle}`)
|
|
416
|
-
if (toggleSectids) buffer.push(':sectids:')
|
|
417
485
|
}
|
|
486
|
+
let hlevel = level + 1
|
|
487
|
+
if (hlevel > 6) {
|
|
488
|
+
hlevel = 6
|
|
489
|
+
buffer.push(syntheticId ? `[discrete#${syntheticId}]` : '[discrete]')
|
|
490
|
+
} else if (syntheticId) {
|
|
491
|
+
buffer.push(`[#${syntheticId}]`)
|
|
492
|
+
}
|
|
493
|
+
buffer.push(`${'='.repeat(hlevel)} ${sectionTitle}`)
|
|
494
|
+
if (toggleSectids) buffer.push(':sectids:')
|
|
418
495
|
}
|
|
419
496
|
}
|
|
420
497
|
|
|
421
498
|
const nextLevel = level + 1
|
|
422
499
|
if (items) {
|
|
423
|
-
|
|
500
|
+
// NOTE: drop first child if same as parent; should we keep if content is different?
|
|
501
|
+
;(urlType === 'internal' && urlType === items[0].urlType && url === items[0].url && !items[0].items
|
|
502
|
+
? items.slice(1)
|
|
503
|
+
: items
|
|
504
|
+
).forEach((item) => {
|
|
424
505
|
buffer.push(
|
|
425
506
|
...aggregateAsciiDoc(
|
|
426
507
|
loadAsciiDoc,
|
|
@@ -432,6 +513,7 @@ function aggregateAsciiDoc (
|
|
|
432
513
|
asciidocConfig,
|
|
433
514
|
mutableAttributes,
|
|
434
515
|
sectionMergeStrategy,
|
|
516
|
+
lastComponentVersion,
|
|
435
517
|
nextLevel
|
|
436
518
|
)
|
|
437
519
|
)
|
|
@@ -440,19 +522,12 @@ function aggregateAsciiDoc (
|
|
|
440
522
|
return buffer
|
|
441
523
|
}
|
|
442
524
|
|
|
443
|
-
function
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
title
|
|
450
|
-
.toLowerCase()
|
|
451
|
-
.replace(/&.+?;|[^ \p{Alpha}0-9_\-.]/gu, '')
|
|
452
|
-
.replace(/[ _.]/g, '-')
|
|
453
|
-
.replace(/--+/g, '-')
|
|
454
|
-
)
|
|
455
|
-
return path.join(...segments)
|
|
525
|
+
function generateSlug (title) {
|
|
526
|
+
return title
|
|
527
|
+
.toLowerCase()
|
|
528
|
+
.replace(/&.+?;|[^ \p{Alpha}0-9_\-.]/gu, '')
|
|
529
|
+
.replace(/[ _.]/g, '-')
|
|
530
|
+
.replace(/--+/g, '-')
|
|
456
531
|
}
|
|
457
532
|
|
|
458
533
|
function fixSectionLevels (sections, multipart) {
|
|
@@ -489,10 +564,10 @@ function rewriteStyleAttribute (block, lines, idx, idprefix, replacementStyle =
|
|
|
489
564
|
let rawStyle
|
|
490
565
|
const commaIdx = prevLine.indexOf(',')
|
|
491
566
|
if (~commaIdx) {
|
|
492
|
-
rawStyle = prevLine.
|
|
567
|
+
rawStyle = prevLine.slice(1, commaIdx - 1)
|
|
493
568
|
if (~rawStyle.indexOf('=')) rawStyle = undefined
|
|
494
569
|
} else if (!~prevLine.indexOf('=')) {
|
|
495
|
-
rawStyle = prevLine.
|
|
570
|
+
rawStyle = prevLine.slice(1, prevLine.length - 2)
|
|
496
571
|
}
|
|
497
572
|
if (rawStyle) {
|
|
498
573
|
if (~rawStyle.indexOf('#')) {
|
|
@@ -500,11 +575,11 @@ function rewriteStyleAttribute (block, lines, idx, idprefix, replacementStyle =
|
|
|
500
575
|
if (replacementStyle) prevLine = prevLine.replace(/^[^#.%,\]]+/, `[${replacementStyle}`)
|
|
501
576
|
} else {
|
|
502
577
|
prevLine = `[${
|
|
503
|
-
replacementStyle ? rawStyle.replace(/^[^.%]
|
|
504
|
-
}#${idprefix}${block.getId()}${prevLine.
|
|
578
|
+
replacementStyle ? rawStyle.replace(/^[^.%]*/, replacementStyle) : rawStyle
|
|
579
|
+
}#${idprefix}${block.getId()}${prevLine.slice(rawStyle.length + 1)}`
|
|
505
580
|
}
|
|
506
581
|
} else {
|
|
507
|
-
prevLine = `[${replacementStyle}#${idprefix}${block.getId()}${rawStyle == null ? ',' : ''}${prevLine.
|
|
582
|
+
prevLine = `[${replacementStyle}#${idprefix}${block.getId()}${rawStyle == null ? ',' : ''}${prevLine.slice(1)}`
|
|
508
583
|
}
|
|
509
584
|
if (cellSpec) prevLine = `${cellSpec}|${prevLine}`
|
|
510
585
|
lines[idx - 1] = prevLine
|
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
+
const File = require('vinyl')
|
|
3
4
|
const filterComponentVersions = require('./filter-component-versions')
|
|
4
5
|
const produceAggregateDocument = require('./produce-aggregate-document')
|
|
5
6
|
const selectMutableAttributes = require('./select-mutable-attributes')
|
|
6
7
|
|
|
8
|
+
const IMAGE_MACRO_RX = /^image::?(.+?)\[(.*?)\]$/
|
|
9
|
+
|
|
7
10
|
function produceAggregateDocuments (loadAsciiDoc, contentCatalog, assemblerConfig) {
|
|
8
11
|
const { insertStartPage, rootLevel, sectionMergeStrategy, asciidoc: assemblerAsciiDocConfig } = assemblerConfig
|
|
9
12
|
const assemblerAsciiDocAttributes = Object.assign({}, assemblerAsciiDocConfig.attributes)
|
|
10
|
-
const { doctype, 'source-highlighter': sourceHighlighter } = assemblerAsciiDocAttributes
|
|
13
|
+
const { doctype, revdate, 'source-highlighter': sourceHighlighter } = assemblerAsciiDocAttributes
|
|
11
14
|
delete assemblerAsciiDocAttributes.doctype
|
|
15
|
+
delete assemblerAsciiDocAttributes.revdate
|
|
12
16
|
delete assemblerAsciiDocAttributes['source-highlighter']
|
|
13
17
|
return filterComponentVersions(contentCatalog.getComponents(), assemblerConfig.componentVersions).reduce(
|
|
14
18
|
(accum, componentVersion) => {
|
|
@@ -16,22 +20,34 @@ function produceAggregateDocuments (loadAsciiDoc, contentCatalog, assemblerConfi
|
|
|
16
20
|
if (!navigation) return accum
|
|
17
21
|
const componentVersionAsciiDocConfig = getAsciiDocConfigWithAsciidoctorReducerExtension(componentVersion)
|
|
18
22
|
const mergedAsciiDocConfig = Object.assign({}, componentVersionAsciiDocConfig, {
|
|
19
|
-
attributes: Object.assign({}, componentVersionAsciiDocConfig.attributes, assemblerAsciiDocAttributes),
|
|
23
|
+
attributes: Object.assign({ revdate }, componentVersionAsciiDocConfig.attributes, assemblerAsciiDocAttributes),
|
|
24
|
+
})
|
|
25
|
+
const mergedAsciiDocAttributes = mergedAsciiDocConfig.attributes
|
|
26
|
+
Object.entries(mergedAsciiDocAttributes).forEach(([name, val]) => {
|
|
27
|
+
const match = name.endsWith('-image') && val.startsWith('image:') && IMAGE_MACRO_RX.exec(val)
|
|
28
|
+
if (!(match && isResourceRef(match[1]))) return
|
|
29
|
+
// Q should we allow image to be resolved relative to component version?
|
|
30
|
+
const image = contentCatalog.resolveResource(match[1], undefined, 'image', ['image'])
|
|
31
|
+
if (!image?.out) return
|
|
32
|
+
mergedAsciiDocAttributes[name] = `image:${image.out.path}[${match[2]}]`
|
|
33
|
+
image.out.assembled = true
|
|
20
34
|
})
|
|
21
35
|
const rootEntry = { content: title }
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
startPageUrl = undefined
|
|
27
|
-
} else {
|
|
28
|
-
Object.assign(rootEntry, { url: startPageUrl, urlType: 'internal' })
|
|
36
|
+
let startPage = contentCatalog.getComponentVersionStartPage(componentName, version)
|
|
37
|
+
if (startPage && startPage.src.component === componentName && startPage.src.version === version) {
|
|
38
|
+
if (insertStartPage && !includedInNav(navigation, startPage.pub.url)) {
|
|
39
|
+
Object.assign(rootEntry, { url: startPage.pub.url, urlType: 'internal' })
|
|
29
40
|
}
|
|
41
|
+
} else {
|
|
42
|
+
// Q: should we always use a reference page as startPage for computing mutableAttributes?
|
|
43
|
+
startPage = createFile({
|
|
44
|
+
component: componentVersion.name,
|
|
45
|
+
version: componentVersion.version,
|
|
46
|
+
relative: '.reference-page.adoc',
|
|
47
|
+
origin: (componentVersion.origins || [])[0],
|
|
48
|
+
})
|
|
30
49
|
}
|
|
31
|
-
|
|
32
|
-
const mutableAttributes = startPage
|
|
33
|
-
? selectMutableAttributes(loadAsciiDoc, contentCatalog, startPage, mergedAsciiDocConfig)
|
|
34
|
-
: {}
|
|
50
|
+
const mutableAttributes = selectMutableAttributes(loadAsciiDoc, contentCatalog, startPage, mergedAsciiDocConfig)
|
|
35
51
|
delete mutableAttributes.doctype
|
|
36
52
|
accum = accum.concat(
|
|
37
53
|
prepareOutlines(navigation, rootEntry, rootLevel).map((outline) =>
|
|
@@ -48,16 +64,40 @@ function produceAggregateDocuments (loadAsciiDoc, contentCatalog, assemblerConfi
|
|
|
48
64
|
)
|
|
49
65
|
)
|
|
50
66
|
)
|
|
51
|
-
|
|
67
|
+
mergedAsciiDocAttributes.doctype = doctype
|
|
52
68
|
sourceHighlighter
|
|
53
|
-
? (
|
|
54
|
-
: delete
|
|
69
|
+
? (mergedAsciiDocAttributes['source-highlighter'] = sourceHighlighter)
|
|
70
|
+
: delete mergedAsciiDocAttributes['source-highlighter']
|
|
55
71
|
return accum
|
|
56
72
|
},
|
|
57
73
|
[]
|
|
58
74
|
)
|
|
59
75
|
}
|
|
60
76
|
|
|
77
|
+
function createFile (src) {
|
|
78
|
+
const familySegment = (src.family ??= 'page') + 's'
|
|
79
|
+
const path = `modules/${(src.module ??= 'ROOT')}/${familySegment}/${src.relative}`
|
|
80
|
+
const moduleRootPath = Array(src.relative.split('/').length - 1)
|
|
81
|
+
.fill('..')
|
|
82
|
+
.join('/')
|
|
83
|
+
const outPath = [
|
|
84
|
+
src.component === 'ROOT' ? '' : src.component,
|
|
85
|
+
src.version,
|
|
86
|
+
src.module === 'ROOT' ? '' : src.module,
|
|
87
|
+
src.family === 'page' ? '' : '_' + familySegment,
|
|
88
|
+
src.family === 'page' ? src.relative.replace(/\.adoc$/, '.html') : src.relative,
|
|
89
|
+
]
|
|
90
|
+
.filter((it) => it)
|
|
91
|
+
.join('/')
|
|
92
|
+
return new File({
|
|
93
|
+
path,
|
|
94
|
+
contents: src.contents ?? Buffer.alloc(0),
|
|
95
|
+
src,
|
|
96
|
+
out: { path: outPath },
|
|
97
|
+
pub: { url: '/' + outPath, moduleRootPath },
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
|
|
61
101
|
function getAsciiDocConfigWithAsciidoctorReducerExtension (componentVersion) {
|
|
62
102
|
const asciidoctorReducerExtension = require('./asciidoctor/reducer-extension') // NOTE: must be required lazily
|
|
63
103
|
const asciidocConfig = componentVersion.asciidoc
|
|
@@ -81,13 +121,20 @@ function includedInNav (items, url) {
|
|
|
81
121
|
return items.find((it) => it.url === url || includedInNav(it.items || [], url))
|
|
82
122
|
}
|
|
83
123
|
|
|
124
|
+
function isResourceRef (target) {
|
|
125
|
+
return ~target.indexOf(':') && !(~target.indexOf('://') || (target.startsWith('data:') && ~target.indexOf(',')))
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// when root level is 0, merge the navigation into the rootEntry
|
|
129
|
+
// when root level is 1, create navigation per navigation menu
|
|
130
|
+
// in this case, if there's only a single navigation menu with no title, promote each top-level item to a menu
|
|
84
131
|
function prepareOutlines (navigation, rootEntry, rootLevel) {
|
|
85
132
|
if (rootLevel === 0 || navigation.length === 1) {
|
|
86
133
|
const navBranch =
|
|
87
134
|
navigation.length === 1
|
|
88
135
|
? navigation[0]
|
|
89
136
|
: { items: navigation.reduce((navTree, it) => navTree.concat(it.content ? it : it.items), []) }
|
|
90
|
-
return [Object.assign(rootEntry, navBranch)]
|
|
137
|
+
return rootLevel === 0 || navBranch.content ? [Object.assign(rootEntry, navBranch)] : navBranch.items
|
|
91
138
|
}
|
|
92
139
|
return navigation.reduce((navTree, it) => navTree.concat(it.content ? it : it.items), [rootEntry])
|
|
93
140
|
}
|
package/lib/util/run-command.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const fs = require('fs')
|
|
3
|
+
const fs = require('node:fs')
|
|
4
4
|
const LazyReadable = require('./lazy-readable')
|
|
5
|
-
const { spawn } = require('child_process')
|
|
5
|
+
const { spawn } = require('node:child_process')
|
|
6
6
|
|
|
7
7
|
const IS_WIN = process.platform === 'win32'
|
|
8
8
|
const DBL_QUOTE_RX = /"/g
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const XML_TAG_RX = /<[^>]+>/g
|
|
4
|
+
const XML_SPECIAL_CHARS = { '<': '<', '>': '>', '&': '&' }
|
|
5
|
+
const XML_SPECIAL_CHARS_RX = /&(?:[lg]t|amp);/g
|
|
6
|
+
|
|
7
|
+
function sanitize (str) {
|
|
8
|
+
return ~str.indexOf('<')
|
|
9
|
+
? [str.replace(XML_TAG_RX, '').replace(XML_SPECIAL_CHARS_RX, (m) => XML_SPECIAL_CHARS[m]), `+++${str}+++`, true]
|
|
10
|
+
: ~str.indexOf('&')
|
|
11
|
+
? Array(2).fill(str.replace(XML_SPECIAL_CHARS_RX, (m) => XML_SPECIAL_CHARS[m]))
|
|
12
|
+
: [str, str]
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
module.exports = sanitize
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@antora/assembler",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.8",
|
|
4
4
|
"description": "An extension library for Antora that assembles content from multiple pages into a single AsciiDoc file to converted and publish.",
|
|
5
5
|
"license": "MPL-2.0",
|
|
6
6
|
"author": "OpenDevise Inc. (https://opendevise.com)",
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
},
|
|
16
16
|
"scripts": {
|
|
17
17
|
"test": "_mocha test",
|
|
18
|
-
"prepublishOnly": "
|
|
19
|
-
"postpublish": "
|
|
18
|
+
"prepublishOnly": "npx -y downdoc --prepublish",
|
|
19
|
+
"postpublish": "npx -y downdoc --postpublish"
|
|
20
20
|
},
|
|
21
21
|
"main": "lib/index.js",
|
|
22
22
|
"exports": {
|
|
@@ -31,13 +31,13 @@
|
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"@antora/expand-path-helper": "~2.0",
|
|
33
33
|
"braces": "~3.0",
|
|
34
|
-
"picomatch": "~
|
|
34
|
+
"picomatch": "~3.0",
|
|
35
35
|
"vinyl": "~2.2",
|
|
36
36
|
"js-yaml": "~4.1"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
|
-
"@antora/asciidoc-loader": "3.
|
|
40
|
-
"@antora/site-publisher": "3.
|
|
39
|
+
"@antora/asciidoc-loader": "~3.1",
|
|
40
|
+
"@antora/site-publisher": "~3.1"
|
|
41
41
|
},
|
|
42
42
|
"engines": {
|
|
43
43
|
"node": ">=16.0.0"
|