@antora/assembler 1.0.0-alpha.7 → 1.0.0-alpha.9
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 +5 -4
- package/lib/filter-component-versions.js +2 -2
- package/lib/load-config.js +3 -2
- package/lib/produce-aggregate-document.js +137 -85
- package/lib/produce-aggregate-documents.js +35 -20
- package/lib/util/sanitize.js +13 -0
- package/lib/util/unconvert-inline-asciidoc.js +95 -0
- package/package.json +8 -5
package/lib/assemble-content.js
CHANGED
|
@@ -3,8 +3,9 @@
|
|
|
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
|
-
async function assembleContent (playbook, contentCatalog,
|
|
8
|
+
async function assembleContent (playbook, contentCatalog, convertDocument, { siteCatalog, configSource }) {
|
|
8
9
|
// Q: could we get ContentCatalog#getComponentVersionStartPage() in Antora core?
|
|
9
10
|
if (typeof contentCatalog.getComponentVersionStartPage !== 'function') {
|
|
10
11
|
contentCatalog.getComponentVersionStartPage = function (component, version) {
|
|
@@ -16,14 +17,14 @@ async function assembleContent (playbook, contentCatalog, converter, { siteCatal
|
|
|
16
17
|
const generatorFunctions = this ? this.getFunctions() : {}
|
|
17
18
|
const { loadAsciiDoc = require('@antora/asciidoc-loader') } = generatorFunctions
|
|
18
19
|
const aggregateDocuments = produceAggregateDocuments(loadAsciiDoc, contentCatalog, assemblerConfig)
|
|
19
|
-
if (!
|
|
20
|
+
if (!convertDocument) return aggregateDocuments
|
|
20
21
|
const { publishSite: publishFiles = require('@antora/site-publisher') } = generatorFunctions
|
|
21
22
|
const buildConfig = assemblerConfig.build
|
|
22
23
|
await prepareWorkspace(publishFiles, aggregateDocuments, contentCatalog, buildConfig)
|
|
23
|
-
// TODO: pass more information to
|
|
24
|
+
// TODO: pass more information to convertDocument 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) => () =>
|
|
27
|
+
.add(aggregateDocuments.map((doc) => () => convertDocument.call(this, doc, buildConfig, runCommand)))
|
|
27
28
|
.toPromise()
|
|
28
29
|
.then((files) => {
|
|
29
30
|
if (buildConfig.publish && siteCatalog) siteCatalog.addFiles(files)
|
|
@@ -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
|
@@ -29,6 +29,7 @@ function loadConfig (playbook, configSource = './antora-assembler.yml') {
|
|
|
29
29
|
if (!('doctype' in asciidocAttrs)) asciidocAttrs.doctype = 'book'
|
|
30
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,7 +63,7 @@ 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
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
const File = require('vinyl')
|
|
4
4
|
const path = require('node:path/posix')
|
|
5
|
+
const sanitize = require('./util/sanitize')
|
|
6
|
+
const unconvertInlineAsciiDoc = require('./util/unconvert-inline-asciidoc')
|
|
5
7
|
|
|
6
8
|
function produceAggregateDocument (
|
|
7
9
|
loadAsciiDoc,
|
|
@@ -14,12 +16,20 @@ function produceAggregateDocument (
|
|
|
14
16
|
mutableAttributes,
|
|
15
17
|
sectionMergeStrategy = 'discrete'
|
|
16
18
|
) {
|
|
17
|
-
const pagesInOutline = selectPagesInOutline(
|
|
18
|
-
outline,
|
|
19
|
-
pages.filter((it) => it.out)
|
|
20
|
-
)
|
|
19
|
+
const pagesInOutline = selectPagesInOutline(outline, pages)
|
|
21
20
|
const navtitle = outline.content
|
|
22
|
-
const
|
|
21
|
+
const templateFile = contentCatalog.addFile({
|
|
22
|
+
src: {
|
|
23
|
+
component: componentVersion.name,
|
|
24
|
+
version: componentVersion.version,
|
|
25
|
+
module: 'ROOT',
|
|
26
|
+
family: 'page',
|
|
27
|
+
relative: generateSlug(navtitle),
|
|
28
|
+
},
|
|
29
|
+
})
|
|
30
|
+
const { dir: outDir, name: outName } = path.parse(templateFile.out.path)
|
|
31
|
+
const path_ = outName === templateFile.src.relative ? path.join(outDir, outName + '.adoc') : outDir + '.adoc'
|
|
32
|
+
contentCatalog.removeFile(templateFile)
|
|
23
33
|
const header = buildAsciiDocHeader(componentVersion, navtitle, doctype)
|
|
24
34
|
const body = aggregateAsciiDoc(
|
|
25
35
|
loadAsciiDoc,
|
|
@@ -32,24 +42,27 @@ function produceAggregateDocument (
|
|
|
32
42
|
mutableAttributes,
|
|
33
43
|
sectionMergeStrategy
|
|
34
44
|
)
|
|
35
|
-
const relativeSrcPath = `${stem}.adoc`
|
|
36
45
|
return new File({
|
|
46
|
+
aggregate: true,
|
|
37
47
|
asciidoc: asciidocConfig,
|
|
38
48
|
contents: Buffer.from([...header, ...body].join('\n') + '\n'),
|
|
39
49
|
mediaType: 'text/asciidoc',
|
|
40
|
-
path:
|
|
50
|
+
path: path_,
|
|
41
51
|
src: {
|
|
42
52
|
component: componentVersion.name,
|
|
43
53
|
version: componentVersion.version,
|
|
44
|
-
basename: path.basename(
|
|
45
|
-
stem,
|
|
54
|
+
basename: path.basename(path_),
|
|
55
|
+
stem: path.basename(path_, '.adoc'),
|
|
46
56
|
extname: '.adoc',
|
|
47
57
|
},
|
|
48
58
|
})
|
|
49
59
|
}
|
|
50
60
|
|
|
51
61
|
function buildAsciiDocHeader (componentVersion, navtitle, doctype = 'book') {
|
|
52
|
-
const
|
|
62
|
+
const navtitlePlain = sanitize(navtitle)
|
|
63
|
+
const navtitleAsciiDoc = unconvertInlineAsciiDoc(navtitle)
|
|
64
|
+
let doctitle = navtitleAsciiDoc
|
|
65
|
+
if (navtitlePlain !== componentVersion.title) doctitle = `${componentVersion.title}: ${doctitle}`
|
|
53
66
|
const version = componentVersion.version === 'master' ? '' : componentVersion.version
|
|
54
67
|
return [
|
|
55
68
|
`= ${doctitle}`,
|
|
@@ -88,16 +101,26 @@ function aggregateAsciiDoc (
|
|
|
88
101
|
asciidocConfig,
|
|
89
102
|
mutableAttributes,
|
|
90
103
|
sectionMergeStrategy,
|
|
104
|
+
lastComponentVersion = componentVersion,
|
|
91
105
|
level = 0
|
|
92
106
|
) {
|
|
93
107
|
const buffer = []
|
|
94
108
|
// TODO: we could try to be smart about it and make sure the page with fragment is included at least once
|
|
95
109
|
if (outlineEntry.hash) return buffer
|
|
96
|
-
const { content: navtitle, items, unresolved, urlType, url } = outlineEntry
|
|
110
|
+
const { content: navtitle, items = [], unresolved, urlType, url } = outlineEntry
|
|
111
|
+
const hasItems = items.length > 0
|
|
112
|
+
const navtitlePlain = sanitize(navtitle)
|
|
113
|
+
const navtitleAsciiDoc = unconvertInlineAsciiDoc(navtitle)
|
|
114
|
+
const siteUrl = ((val) => {
|
|
115
|
+
if (!val || val === '/') return ''
|
|
116
|
+
return val.charAt(val.length - 1) === '/' ? val.slice(0, val.length - 1) : val
|
|
117
|
+
})(asciidocConfig.attributes['site-url'])
|
|
97
118
|
// FIXME: ideally, resource ID would be stored in navigation so we can look up the page more efficiently
|
|
98
119
|
let page = urlType === 'internal' && !unresolved ? pagesInOutline.get(url) : undefined
|
|
120
|
+
if (page && pagesInOutline.aggregated?.includes(page)) page = undefined
|
|
99
121
|
if (page) {
|
|
100
122
|
let contents = page.src.contents
|
|
123
|
+
if (contents == null) return buffer
|
|
101
124
|
// NOTE: blank lines at top and bottom of document create mismatch when using line numbers to navigate source lines
|
|
102
125
|
// IMPORTANT: this must not leave behind lines the parser will drop!
|
|
103
126
|
// IDEA: another option is to capture initial lineno of reader and use as offset (but preseves those blank lines)
|
|
@@ -107,16 +130,36 @@ function aggregateAsciiDoc (
|
|
|
107
130
|
.replace(/^(?:[ \t]*\r\n?|[ \t]*\n)+/, '')
|
|
108
131
|
.trimRight()
|
|
109
132
|
)
|
|
133
|
+
;(pagesInOutline.aggregated ??= []).push(page)
|
|
110
134
|
page = new page.constructor(Object.assign({}, page, { contents, mediaType: 'text/asciidoc' }))
|
|
111
|
-
const { module: module_, relative, origin } = page.src
|
|
135
|
+
const { component, version, module: module_, relative, origin } = page.src
|
|
112
136
|
const doc = loadAsciiDoc(page, contentCatalog, asciidocConfig)
|
|
113
137
|
const refs = doc.getCatalog().refs
|
|
114
138
|
// NOTE: in Antora, docname is relative src path from module without file extension
|
|
115
139
|
const docname = doc.getAttribute('docname')
|
|
116
140
|
const docnameForId = docname.replace(/[/]/g, '::').replace(/[.]/g, '-')
|
|
117
|
-
const
|
|
141
|
+
const scopeId = component !== componentVersion.name
|
|
142
|
+
const idprefix =
|
|
143
|
+
(scopeId ? component + ':' : '') +
|
|
144
|
+
(module_ === 'ROOT' ? (scopeId ? ':' : '') : module_ + ':') +
|
|
145
|
+
docnameForId +
|
|
146
|
+
':::'
|
|
118
147
|
buffer.push('')
|
|
119
148
|
buffer.push(`:docname: ${docname}`)
|
|
149
|
+
if (component !== lastComponentVersion.name) {
|
|
150
|
+
const thisComponentVersion =
|
|
151
|
+
component === componentVersion.name && version === componentVersion.version
|
|
152
|
+
? componentVersion
|
|
153
|
+
: contentCatalog.getComponentVersion(component, version)
|
|
154
|
+
if (thisComponentVersion) {
|
|
155
|
+
buffer.push(`:page-component-name: ${thisComponentVersion.name}`)
|
|
156
|
+
buffer.push(`:page-component-version:${thisComponentVersion.version ? ' ' + thisComponentVersion.version : ''}`)
|
|
157
|
+
buffer.push(':page-version: {page-component-version}')
|
|
158
|
+
buffer.push(`:page-component-display-version: ${thisComponentVersion.displayVersion}`)
|
|
159
|
+
buffer.push(`:page-component-title: ${thisComponentVersion.title}`)
|
|
160
|
+
lastComponentVersion = thisComponentVersion
|
|
161
|
+
}
|
|
162
|
+
}
|
|
120
163
|
buffer.push(`:page-module: ${module_}`)
|
|
121
164
|
buffer.push(`:page-relative-src-path: ${relative}`)
|
|
122
165
|
//buffer.push(`:page-origin-type: ${origin.type}`)
|
|
@@ -128,7 +171,7 @@ function aggregateAsciiDoc (
|
|
|
128
171
|
let enclosed
|
|
129
172
|
// NOTE: if level is 0, doctitle has already been added and we're in the document header
|
|
130
173
|
if (level) {
|
|
131
|
-
if (level === 1 &&
|
|
174
|
+
if (level === 1 && navtitlePlain === componentVersion.title) {
|
|
132
175
|
level--
|
|
133
176
|
} else {
|
|
134
177
|
let hlevel = level + 1
|
|
@@ -138,12 +181,12 @@ function aggregateAsciiDoc (
|
|
|
138
181
|
} else {
|
|
139
182
|
buffer.push(`[#${idprefix}]`)
|
|
140
183
|
}
|
|
141
|
-
buffer.push(`${'='.repeat(hlevel)} ${
|
|
184
|
+
buffer.push(`${'='.repeat(hlevel)} ${navtitleAsciiDoc}`)
|
|
142
185
|
}
|
|
143
186
|
} else {
|
|
144
187
|
header.unshift(`[#${idprefix}]`)
|
|
145
188
|
}
|
|
146
|
-
if (sectionMergeStrategy === 'enclose' &&
|
|
189
|
+
if (sectionMergeStrategy === 'enclose' && hasItems && doc.hasSections()) {
|
|
147
190
|
enclosed = true
|
|
148
191
|
// TODO: make overview section title configurable
|
|
149
192
|
//let overviewTitle = doc.getDocumentTitle()
|
|
@@ -170,10 +213,6 @@ function aggregateAsciiDoc (
|
|
|
170
213
|
buffer.push(`${'='.repeat(hlevel)} ${overviewTitle}`)
|
|
171
214
|
if (toggleSectids) buffer.push(':sectids:')
|
|
172
215
|
}
|
|
173
|
-
const siteUrl = ((val) => {
|
|
174
|
-
if (!val || val === '/') return ''
|
|
175
|
-
return val.charAt(val.length - 1) === '/' ? val.substr(0, val.length - 1) : val
|
|
176
|
-
})(doc.getAttribute('site-url'))
|
|
177
216
|
const lines = doc.getSourceLines()
|
|
178
217
|
const ignoreLines = []
|
|
179
218
|
// TODO: think more about when multipart is allowed; perhaps configurable
|
|
@@ -250,8 +289,8 @@ function aggregateAsciiDoc (
|
|
|
250
289
|
let pagePart, fragment, targetPage
|
|
251
290
|
const hashIdx = target.indexOf('#')
|
|
252
291
|
if (~hashIdx) {
|
|
253
|
-
pagePart = target.
|
|
254
|
-
fragment = target.
|
|
292
|
+
pagePart = target.slice(0, hashIdx)
|
|
293
|
+
fragment = target.slice(hashIdx + 1)
|
|
255
294
|
// TODO: for now, assume .adoc; in the future, consider other file extensions
|
|
256
295
|
if (pagePart && !pagePart.endsWith('.adoc')) pagePart += '.adoc'
|
|
257
296
|
} else if (target.endsWith('.adoc')) {
|
|
@@ -267,20 +306,24 @@ function aggregateAsciiDoc (
|
|
|
267
306
|
: `<<${idprefix}${fragment}${text ? ',' + text.replace(/\\]/g, ']') : ''}>>`
|
|
268
307
|
}
|
|
269
308
|
if (~pagePart.indexOf('@') || /:.*:/.test(pagePart)) {
|
|
309
|
+
if (siteUrl && (targetPage = contentCatalog.resolvePage(pagePart, page.src)) && targetPage.out) {
|
|
310
|
+
text ||= targetPage.asciidoc?.xreftext || target
|
|
311
|
+
return `${siteUrl}${targetPage.pub.url}${fragment && '#' + fragment}[${text}]`
|
|
312
|
+
}
|
|
270
313
|
// TODO: handle unresolved page better
|
|
271
|
-
return
|
|
272
|
-
? `${siteUrl}${targetPage.pub.url}${fragment && '#' + fragment}[${text}]`
|
|
273
|
-
: m
|
|
314
|
+
return m
|
|
274
315
|
} else if (pagePart.indexOf(':') < 0) {
|
|
275
316
|
if (module_ !== 'ROOT') pagePart = `${module_}:${pagePart}`
|
|
276
317
|
} else if (pagePart.startsWith('ROOT:')) {
|
|
277
|
-
pagePart = pagePart.
|
|
318
|
+
pagePart = pagePart.slice(5)
|
|
278
319
|
}
|
|
279
320
|
if (!(targetPage = pagesInOutline.get(pagePart))) {
|
|
321
|
+
if (siteUrl && (targetPage = contentCatalog.resolvePage(pagePart, page.src)) && targetPage.out) {
|
|
322
|
+
text ||= targetPage.asciidoc?.xreftext || target
|
|
323
|
+
return `${siteUrl}${targetPage.pub.url}${fragment && '#' + fragment}[${text}]`
|
|
324
|
+
}
|
|
280
325
|
// TODO: handle unresolved page better
|
|
281
|
-
return
|
|
282
|
-
? `${siteUrl}${targetPage.pub.url}${fragment && '#' + fragment}[${text}]`
|
|
283
|
-
: m
|
|
326
|
+
return m
|
|
284
327
|
}
|
|
285
328
|
pagePart = pagePart
|
|
286
329
|
.replace(/[/]/g, '::')
|
|
@@ -301,9 +344,8 @@ function aggregateAsciiDoc (
|
|
|
301
344
|
family: 'attachment',
|
|
302
345
|
relative,
|
|
303
346
|
})
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
: m
|
|
347
|
+
// TODO: handle unresolved attachment page
|
|
348
|
+
return attachment?.out ? `${siteUrl}${attachment.pub.url.replace(/_/g, '{underscore}')}[${text}]` : m
|
|
307
349
|
})
|
|
308
350
|
}
|
|
309
351
|
if (~line.indexOf('image:') && !line.startsWith('image::')) {
|
|
@@ -311,7 +353,7 @@ function aggregateAsciiDoc (
|
|
|
311
353
|
if (isResourceSpec(target)) {
|
|
312
354
|
const image = contentCatalog.resolveResource(target, page.src, 'image', ['image'])
|
|
313
355
|
// TODO: handle (or report) unresolved image better
|
|
314
|
-
if (image
|
|
356
|
+
if (image?.out) {
|
|
315
357
|
image.out.assembled = true
|
|
316
358
|
return `image:${image.out.path.replace(/_/g, '{underscore}')}[${attrlist}]`
|
|
317
359
|
}
|
|
@@ -353,14 +395,14 @@ function aggregateAsciiDoc (
|
|
|
353
395
|
let prefix = ''
|
|
354
396
|
// Q: can we use startsWith('image::') in certain cases?
|
|
355
397
|
let imageMacroOffset = (
|
|
356
|
-
lastImageMacroAt?.[0] === idx ? line.
|
|
398
|
+
lastImageMacroAt?.[0] === idx ? line.slice(0, lastImageMacroAt[1]) : line
|
|
357
399
|
).lastIndexOf('image::')
|
|
358
400
|
if (imageMacroOffset > 0) {
|
|
359
401
|
if (
|
|
360
402
|
block.getDocument().isNested() &&
|
|
361
|
-
(prefix = line.
|
|
403
|
+
(prefix = line.slice(0, imageMacroOffset)).trimRight().endsWith('|')
|
|
362
404
|
) {
|
|
363
|
-
line = line.
|
|
405
|
+
line = line.slice(prefix.length)
|
|
364
406
|
} else {
|
|
365
407
|
imageMacroOffset = -1
|
|
366
408
|
}
|
|
@@ -370,8 +412,8 @@ function aggregateAsciiDoc (
|
|
|
370
412
|
if (isResourceSpec(target)) {
|
|
371
413
|
const image = contentCatalog.resolveResource(target, page.src, 'image', ['image'])
|
|
372
414
|
// FIXME: handle (or report) case when image is not resolved
|
|
373
|
-
if (image
|
|
374
|
-
const boxedAttrlist = line.
|
|
415
|
+
if (image?.out) {
|
|
416
|
+
const boxedAttrlist = line.slice(line.indexOf('['))
|
|
375
417
|
image.out.assembled = true
|
|
376
418
|
lines[idx] = `${prefix}image::${image.out.path}${boxedAttrlist}`
|
|
377
419
|
}
|
|
@@ -398,7 +440,15 @@ function aggregateAsciiDoc (
|
|
|
398
440
|
} else if (val !== initialVal) {
|
|
399
441
|
accum.push(`:${name}:${initialVal ? ' ' + initialVal : ''}`)
|
|
400
442
|
}
|
|
401
|
-
} else if (
|
|
443
|
+
} else if (
|
|
444
|
+
!(
|
|
445
|
+
val == null ||
|
|
446
|
+
doc.isAttributeLocked(name) ||
|
|
447
|
+
name === 'doctype' ||
|
|
448
|
+
name === 'leveloffset' ||
|
|
449
|
+
name === 'underscore'
|
|
450
|
+
)
|
|
451
|
+
) {
|
|
402
452
|
accum.push(`:!${name}:`)
|
|
403
453
|
}
|
|
404
454
|
return accum
|
|
@@ -407,43 +457,51 @@ function aggregateAsciiDoc (
|
|
|
407
457
|
)
|
|
408
458
|
if (resolvedAttributeEntries.length > 1) buffer.push(...resolvedAttributeEntries)
|
|
409
459
|
}
|
|
410
|
-
} else {
|
|
411
|
-
if (level) {
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
460
|
+
} else if (level) {
|
|
461
|
+
if (level === 1 && navtitlePlain === componentVersion.title) {
|
|
462
|
+
level--
|
|
463
|
+
} else {
|
|
464
|
+
buffer.push('')
|
|
465
|
+
// NOTE: try to toggle sectids; otherwise, fallback to globally unique synthetic ID
|
|
466
|
+
// Q: should we unset docname, page-module, etc?
|
|
467
|
+
let toggleSectids, syntheticId
|
|
468
|
+
if (!('sectids' in asciidocConfig.attributes)) {
|
|
469
|
+
buffer.push(':!sectids:')
|
|
470
|
+
toggleSectids = true
|
|
471
|
+
} else if (typeof asciidocConfig.attributes.sectids === 'string') {
|
|
472
|
+
if ('sectids' in mutableAttributes) {
|
|
419
473
|
buffer.push(':!sectids:')
|
|
420
474
|
toggleSectids = true
|
|
421
|
-
} else
|
|
422
|
-
|
|
423
|
-
buffer.push(':!sectids:')
|
|
424
|
-
toggleSectids = true
|
|
425
|
-
} else {
|
|
426
|
-
syntheticId = `__object-id-${global.Opal.hash(outlineEntry).$object_id()}`
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
// Q: should we unset docname, page-module, etc?
|
|
430
|
-
const sectionTitle = urlType === 'external' ? `${url}[${navtitle.replace(/\]/, '\\]')}]` : navtitle
|
|
431
|
-
let hlevel = level + 1
|
|
432
|
-
if (hlevel > 6) {
|
|
433
|
-
hlevel = 6
|
|
434
|
-
buffer.push(syntheticId ? `[discrete#${syntheticId}]` : '[discrete]')
|
|
435
|
-
} else if (syntheticId) {
|
|
436
|
-
buffer.push(`[#${syntheticId}]`)
|
|
475
|
+
} else {
|
|
476
|
+
syntheticId = `__object-id-${global.Opal.hash(outlineEntry).$object_id()}`
|
|
437
477
|
}
|
|
438
|
-
buffer.push(`${'='.repeat(hlevel)} ${sectionTitle}`)
|
|
439
|
-
if (toggleSectids) buffer.push(':sectids:')
|
|
440
478
|
}
|
|
479
|
+
let sectionTitle = navtitleAsciiDoc
|
|
480
|
+
if (urlType === 'external') {
|
|
481
|
+
sectionTitle = `${url}[${navtitleAsciiDoc.replace(/\]/g, '\\]')}]`
|
|
482
|
+
} else if (urlType === 'internal' && !unresolved && siteUrl) {
|
|
483
|
+
const resource = contentCatalog.getFiles().find((it) => it.out && it.pub.url === url)
|
|
484
|
+
if (resource) sectionTitle = `${siteUrl}${resource.pub.url}[${navtitleAsciiDoc.replace(/\]/g, '\\]')}]`
|
|
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:')
|
|
441
495
|
}
|
|
442
496
|
}
|
|
443
497
|
|
|
444
498
|
const nextLevel = level + 1
|
|
445
|
-
if (
|
|
446
|
-
|
|
499
|
+
if (hasItems) {
|
|
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) => {
|
|
447
505
|
buffer.push(
|
|
448
506
|
...aggregateAsciiDoc(
|
|
449
507
|
loadAsciiDoc,
|
|
@@ -455,6 +513,7 @@ function aggregateAsciiDoc (
|
|
|
455
513
|
asciidocConfig,
|
|
456
514
|
mutableAttributes,
|
|
457
515
|
sectionMergeStrategy,
|
|
516
|
+
lastComponentVersion,
|
|
458
517
|
nextLevel
|
|
459
518
|
)
|
|
460
519
|
)
|
|
@@ -463,19 +522,12 @@ function aggregateAsciiDoc (
|
|
|
463
522
|
return buffer
|
|
464
523
|
}
|
|
465
524
|
|
|
466
|
-
function
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
title
|
|
473
|
-
.toLowerCase()
|
|
474
|
-
.replace(/&.+?;|[^ \p{Alpha}0-9_\-.]/gu, '')
|
|
475
|
-
.replace(/[ _.]/g, '-')
|
|
476
|
-
.replace(/--+/g, '-')
|
|
477
|
-
)
|
|
478
|
-
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, '-')
|
|
479
531
|
}
|
|
480
532
|
|
|
481
533
|
function fixSectionLevels (sections, multipart) {
|
|
@@ -512,10 +564,10 @@ function rewriteStyleAttribute (block, lines, idx, idprefix, replacementStyle =
|
|
|
512
564
|
let rawStyle
|
|
513
565
|
const commaIdx = prevLine.indexOf(',')
|
|
514
566
|
if (~commaIdx) {
|
|
515
|
-
rawStyle = prevLine.
|
|
567
|
+
rawStyle = prevLine.slice(1, commaIdx - 1)
|
|
516
568
|
if (~rawStyle.indexOf('=')) rawStyle = undefined
|
|
517
569
|
} else if (!~prevLine.indexOf('=')) {
|
|
518
|
-
rawStyle = prevLine.
|
|
570
|
+
rawStyle = prevLine.slice(1, prevLine.length - 2)
|
|
519
571
|
}
|
|
520
572
|
if (rawStyle) {
|
|
521
573
|
if (~rawStyle.indexOf('#')) {
|
|
@@ -524,10 +576,10 @@ function rewriteStyleAttribute (block, lines, idx, idprefix, replacementStyle =
|
|
|
524
576
|
} else {
|
|
525
577
|
prevLine = `[${
|
|
526
578
|
replacementStyle ? rawStyle.replace(/^[^.%]*/, replacementStyle) : rawStyle
|
|
527
|
-
}#${idprefix}${block.getId()}${prevLine.
|
|
579
|
+
}#${idprefix}${block.getId()}${prevLine.slice(rawStyle.length + 1)}`
|
|
528
580
|
}
|
|
529
581
|
} else {
|
|
530
|
-
prevLine = `[${replacementStyle}#${idprefix}${block.getId()}${rawStyle == null ? ',' : ''}${prevLine.
|
|
582
|
+
prevLine = `[${replacementStyle}#${idprefix}${block.getId()}${rawStyle == null ? ',' : ''}${prevLine.slice(1)}`
|
|
531
583
|
}
|
|
532
584
|
if (cellSpec) prevLine = `${cellSpec}|${prevLine}`
|
|
533
585
|
lines[idx - 1] = prevLine
|
|
@@ -5,6 +5,8 @@ const filterComponentVersions = require('./filter-component-versions')
|
|
|
5
5
|
const produceAggregateDocument = require('./produce-aggregate-document')
|
|
6
6
|
const selectMutableAttributes = require('./select-mutable-attributes')
|
|
7
7
|
|
|
8
|
+
const IMAGE_MACRO_RX = /^image::?(.+?)\[(.*?)\]$/
|
|
9
|
+
|
|
8
10
|
function produceAggregateDocuments (loadAsciiDoc, contentCatalog, assemblerConfig) {
|
|
9
11
|
const { insertStartPage, rootLevel, sectionMergeStrategy, asciidoc: assemblerAsciiDocConfig } = assemblerConfig
|
|
10
12
|
const assemblerAsciiDocAttributes = Object.assign({}, assemblerAsciiDocConfig.attributes)
|
|
@@ -20,26 +22,32 @@ function produceAggregateDocuments (loadAsciiDoc, contentCatalog, assemblerConfi
|
|
|
20
22
|
const mergedAsciiDocConfig = Object.assign({}, componentVersionAsciiDocConfig, {
|
|
21
23
|
attributes: Object.assign({ revdate }, componentVersionAsciiDocConfig.attributes, assemblerAsciiDocAttributes),
|
|
22
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
|
|
34
|
+
})
|
|
23
35
|
const rootEntry = { content: title }
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
startPageUrl = undefined
|
|
29
|
-
} else {
|
|
30
|
-
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' })
|
|
31
40
|
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
startPage ??
|
|
36
|
-
createFile({
|
|
41
|
+
} else {
|
|
42
|
+
// Q: should we always use a reference page as startPage for computing mutableAttributes?
|
|
43
|
+
startPage = createFile({
|
|
37
44
|
component: componentVersion.name,
|
|
38
45
|
version: componentVersion.version,
|
|
39
46
|
relative: '.reference-page.adoc',
|
|
40
47
|
origin: (componentVersion.origins || [])[0],
|
|
41
48
|
})
|
|
42
|
-
|
|
49
|
+
}
|
|
50
|
+
const mutableAttributes = selectMutableAttributes(loadAsciiDoc, contentCatalog, startPage, mergedAsciiDocConfig)
|
|
43
51
|
delete mutableAttributes.doctype
|
|
44
52
|
accum = accum.concat(
|
|
45
53
|
prepareOutlines(navigation, rootEntry, rootLevel).map((outline) =>
|
|
@@ -56,10 +64,10 @@ function produceAggregateDocuments (loadAsciiDoc, contentCatalog, assemblerConfi
|
|
|
56
64
|
)
|
|
57
65
|
)
|
|
58
66
|
)
|
|
59
|
-
|
|
67
|
+
mergedAsciiDocAttributes.doctype = doctype
|
|
60
68
|
sourceHighlighter
|
|
61
|
-
? (
|
|
62
|
-
: delete
|
|
69
|
+
? (mergedAsciiDocAttributes['source-highlighter'] = sourceHighlighter)
|
|
70
|
+
: delete mergedAsciiDocAttributes['source-highlighter']
|
|
63
71
|
return accum
|
|
64
72
|
},
|
|
65
73
|
[]
|
|
@@ -113,15 +121,22 @@ function includedInNav (items, url) {
|
|
|
113
121
|
return items.find((it) => it.url === url || includedInNav(it.items || [], url))
|
|
114
122
|
}
|
|
115
123
|
|
|
124
|
+
function isResourceRef (target) {
|
|
125
|
+
return ~target.indexOf(':') && !(~target.indexOf('://') || (target.startsWith('data:') && ~target.indexOf(',')))
|
|
126
|
+
}
|
|
127
|
+
|
|
116
128
|
// when root level is 0, merge the navigation into the rootEntry
|
|
117
129
|
// when root level is 1, create navigation per navigation menu
|
|
118
130
|
// in this case, if there's only a single navigation menu with no title, promote each top-level item to a menu
|
|
119
131
|
function prepareOutlines (navigation, rootEntry, rootLevel) {
|
|
120
132
|
if (rootLevel === 0 || navigation.length === 1) {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
133
|
+
let navBranch
|
|
134
|
+
if (navigation.length === 1) {
|
|
135
|
+
navBranch = navigation[0]
|
|
136
|
+
} else {
|
|
137
|
+
const items = navigation.reduce((accum, it) => accum.concat(it.content ? it : it.items), [])
|
|
138
|
+
navBranch = items.length ? { items } : {}
|
|
139
|
+
}
|
|
125
140
|
return rootLevel === 0 || navBranch.content ? [Object.assign(rootEntry, navBranch)] : navBranch.items
|
|
126
141
|
}
|
|
127
142
|
return navigation.reduce((navTree, it) => navTree.concat(it.content ? it : it.items), [rootEntry])
|
|
@@ -0,0 +1,13 @@
|
|
|
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
|
+
if (~str.indexOf('<')) str = str.replace(XML_TAG_RX, '')
|
|
9
|
+
if (~str.indexOf('&')) str = str.replace(XML_SPECIAL_CHARS_RX, (m) => XML_SPECIAL_CHARS[m])
|
|
10
|
+
return str
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
module.exports = sanitize
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const ATTRIBUTE_REFERENCE_RX = /\{[a-z0-9_][a-z0-9_-]*\}/g
|
|
4
|
+
const STRICT_WORD_CHAR_RX = /[\p{L}\d_]/u
|
|
5
|
+
const WORD_CHAR_RX = /[\p{L}\d_;}:<>]/u
|
|
6
|
+
|
|
7
|
+
const MARK_FOR_TAG = { code: '`', em: '_', mark: '#', span: '#', strong: '*' }
|
|
8
|
+
const SKIP_SPAN = { icon: '<i ', image: '<img ' }
|
|
9
|
+
|
|
10
|
+
module.exports = (str) => {
|
|
11
|
+
if (!str) return str
|
|
12
|
+
let matchIndex = str.indexOf('<')
|
|
13
|
+
if (!~matchIndex) return ~str.indexOf('{') ? str.replace(ATTRIBUTE_REFERENCE_RX, '\\$&') : str
|
|
14
|
+
let current = { contents: '' }
|
|
15
|
+
const stack = [current]
|
|
16
|
+
let lastIndex = 0
|
|
17
|
+
do {
|
|
18
|
+
if (matchIndex > lastIndex) {
|
|
19
|
+
const matched = str.slice(lastIndex, matchIndex)
|
|
20
|
+
current.contents += ~matched.indexOf('{') ? matched.replace(ATTRIBUTE_REFERENCE_RX, '\\$&') : matched
|
|
21
|
+
}
|
|
22
|
+
const isCloseTag = str[++matchIndex] === '/' ? ++matchIndex : false
|
|
23
|
+
let tagName = str.slice(matchIndex, (lastIndex = str.indexOf('>', matchIndex) + 1) - 1)
|
|
24
|
+
if (isCloseTag) {
|
|
25
|
+
const parent = current // TODO expect tagName to equal current.tagName
|
|
26
|
+
stack.pop()
|
|
27
|
+
current = stack[stack.length - 1]
|
|
28
|
+
if (parent.mark) {
|
|
29
|
+
let { contents, mark, id, role } = parent
|
|
30
|
+
const attrlist = (id ? '#' + id : '') + (role ? '.' + role.replace(/ /g, '.') : '')
|
|
31
|
+
if (
|
|
32
|
+
current.mark === mark ||
|
|
33
|
+
current.mark === '_' ||
|
|
34
|
+
isWordChar(str.charAt(lastIndex), true) ||
|
|
35
|
+
isWordChar(current.contents[current.contents.length - 1] || current.mark)
|
|
36
|
+
) {
|
|
37
|
+
mark = mark.repeat(2)
|
|
38
|
+
}
|
|
39
|
+
current.contents += (attrlist ? '[' + attrlist + ']' : '') + mark + contents + mark
|
|
40
|
+
} else {
|
|
41
|
+
current.contents += parent.contents
|
|
42
|
+
}
|
|
43
|
+
} else {
|
|
44
|
+
let attrs, attrlistIndex, role
|
|
45
|
+
if (~(attrlistIndex = tagName.indexOf(' '))) {
|
|
46
|
+
role = (attrs = parseAttrlist(tagName.slice(attrlistIndex))).class
|
|
47
|
+
tagName = tagName.slice(0, attrlistIndex)
|
|
48
|
+
}
|
|
49
|
+
if (tagName === 'img') {
|
|
50
|
+
current.contents += 'image:' + attrs.src + '[' + attrs.alt + ']'
|
|
51
|
+
} else if (tagName === 'i' && current.tagName === 'span' && current.role === 'icon') {
|
|
52
|
+
current.contents += 'icon:' + role.slice(6) + '[]'
|
|
53
|
+
lastIndex += 4
|
|
54
|
+
} else {
|
|
55
|
+
let check, mark
|
|
56
|
+
const id = attrs?.id
|
|
57
|
+
if (tagName !== 'span' || id || (role && !((check = SKIP_SPAN[role]) && str.startsWith(check, lastIndex)))) {
|
|
58
|
+
mark = MARK_FOR_TAG[tagName]
|
|
59
|
+
}
|
|
60
|
+
stack.push((current = { tagName, role, id, mark, contents: '' }))
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
} while (~(matchIndex = str.indexOf('<', lastIndex)))
|
|
64
|
+
const rest = str.slice(lastIndex)
|
|
65
|
+
if (rest) current.contents += ~rest.indexOf('{') ? rest.replace(ATTRIBUTE_REFERENCE_RX, '\\$&') : rest
|
|
66
|
+
return current.contents
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function parseAttrlist (str) {
|
|
70
|
+
let lastIndex = 0
|
|
71
|
+
const attrs = {}
|
|
72
|
+
while (str.charAt(lastIndex++) === ' ') {
|
|
73
|
+
const spaceIndex = str.indexOf(' ', lastIndex)
|
|
74
|
+
const equalsIndex = str.indexOf('=', lastIndex)
|
|
75
|
+
if (~spaceIndex && spaceIndex < equalsIndex) {
|
|
76
|
+
attrs[str.slice(lastIndex, (lastIndex = spaceIndex))] = true
|
|
77
|
+
} else if (~equalsIndex) {
|
|
78
|
+
const name = str.slice(lastIndex, equalsIndex)
|
|
79
|
+
const valueIndex = equalsIndex + 1
|
|
80
|
+
attrs[name] =
|
|
81
|
+
str.charAt(valueIndex) === '"'
|
|
82
|
+
? str.slice(valueIndex + 1, (lastIndex = str.indexOf('"', valueIndex + 1) + 1) - 1)
|
|
83
|
+
: str.slice(valueIndex, (lastIndex = ~spaceIndex ? spaceIndex : str.length))
|
|
84
|
+
} else {
|
|
85
|
+
attrs[str.slice(lastIndex)] = true
|
|
86
|
+
break
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return attrs
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function isWordChar (str, strict) {
|
|
93
|
+
if (!str) return false
|
|
94
|
+
return (strict ? STRICT_WORD_CHAR_RX : WORD_CHAR_RX).test(str)
|
|
95
|
+
}
|
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.9",
|
|
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": {
|
|
@@ -28,6 +28,9 @@
|
|
|
28
28
|
"./produce-aggregate-documents": "./lib/produce-aggregate-documents.js",
|
|
29
29
|
"./select-mutable-attributes": "./lib/select-mutable-attributes.js"
|
|
30
30
|
},
|
|
31
|
+
"imports": {
|
|
32
|
+
"#unconvert-inline-asciidoc": "./lib/util/unconvert-inline-asciidoc.js"
|
|
33
|
+
},
|
|
31
34
|
"dependencies": {
|
|
32
35
|
"@antora/expand-path-helper": "~2.0",
|
|
33
36
|
"braces": "~3.0",
|
|
@@ -36,8 +39,8 @@
|
|
|
36
39
|
"js-yaml": "~4.1"
|
|
37
40
|
},
|
|
38
41
|
"devDependencies": {
|
|
39
|
-
"@antora/asciidoc-loader": "3.
|
|
40
|
-
"@antora/site-publisher": "3.
|
|
42
|
+
"@antora/asciidoc-loader": "~3.1",
|
|
43
|
+
"@antora/site-publisher": "~3.1"
|
|
41
44
|
},
|
|
42
45
|
"engines": {
|
|
43
46
|
"node": ">=16.0.0"
|