@antora/assembler 1.0.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -0
- package/lib/asciidoctor/reducer-extension.js +230 -0
- package/lib/assemble-content.js +45 -0
- package/lib/filter-component-versions.js +71 -0
- package/lib/index.js +6 -0
- package/lib/load-config.js +62 -0
- package/lib/produce-aggregate-document.js +520 -0
- package/lib/produce-aggregate-documents.js +83 -0
- package/lib/select-mutable-attributes.js +26 -0
- package/lib/util/lazy-readable.js +19 -0
- package/lib/util/promise-queue.js +57 -0
- package/lib/util/run-command.js +56 -0
- package/package.json +57 -0
package/README.md
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Antora Assembler
|
|
2
|
+
|
|
3
|
+
A library for writing Antora extensions that combine multiple pages into one.
|
|
4
|
+
|
|
5
|
+
Assembler works by constructing aggregate AsciiDoc documents from the pages based on the navigation tree per component version.
|
|
6
|
+
It then invokes the specified callback to convert those documents to an output format.
|
|
7
|
+
If a catalog is specified, Assembler pushes the converted files back into the catalog to be published as attachments alongside the other files in the site.
|
|
8
|
+
|
|
9
|
+
[Antora](https://antora.org) is a modular static site generator designed for creating documentation sites from AsciiDoc documents.
|
|
10
|
+
The Assembler extends the feature set of Antora by providing the foundation for page aggregation.
|
|
11
|
+
|
|
12
|
+
## Copyright and License
|
|
13
|
+
|
|
14
|
+
Copyright (C) 2022-present by OpenDevise Inc. and the individual contributors of this project.
|
|
15
|
+
|
|
16
|
+
Use of this software is granted under the terms of the [Mozilla Public License Version 2.0](https://www.mozilla.org/en-US/MPL/2.0/) (MPL-2.0).
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const Opal = global.Opal
|
|
4
|
+
const Asciidoctor = Opal.Asciidoctor
|
|
5
|
+
const $Reducer = function $Reducer () {}
|
|
6
|
+
|
|
7
|
+
const DocumentExt = (() => {
|
|
8
|
+
const parentScope = Opal.module(Asciidoctor, 'Reducer', $Reducer)
|
|
9
|
+
const scope = Opal.module(parentScope, 'DocumentExt', function $DocumentExt () {})
|
|
10
|
+
|
|
11
|
+
Opal.defn(scope, '$save_attributes', function saveAttributes () {
|
|
12
|
+
this.attributes_defined_in_header = this.attributes_modified.$to_a().reduce((accum, name) => {
|
|
13
|
+
accum[name] = this.getAttribute(name)
|
|
14
|
+
return accum
|
|
15
|
+
}, {})
|
|
16
|
+
return Opal.send(this, Opal.find_super_dispatcher(this, 'save_attributes', saveAttributes), [])
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
return scope
|
|
20
|
+
})()
|
|
21
|
+
|
|
22
|
+
const ConditionalDirectiveTracker = (() => {
|
|
23
|
+
const parentScope = Opal.module(Asciidoctor, 'Reducer', $Reducer)
|
|
24
|
+
const scope = Opal.module(parentScope, 'ConditionalDirectiveTracker', function $ConditionalDirectiveTracker () {})
|
|
25
|
+
|
|
26
|
+
Opal.defn(
|
|
27
|
+
scope,
|
|
28
|
+
'$preprocess_conditional_directive',
|
|
29
|
+
function preprocessConditionalDirective (keyword, target, delimiter, text) {
|
|
30
|
+
const skipActive = this.skipping
|
|
31
|
+
const depth = this.conditional_stack.length
|
|
32
|
+
let directiveLineno = this.lineno
|
|
33
|
+
const result = Opal.send(
|
|
34
|
+
this,
|
|
35
|
+
Opal.find_super_dispatcher(this, 'preprocess_conditional_directive', preprocessConditionalDirective),
|
|
36
|
+
[keyword, target, delimiter, text]
|
|
37
|
+
)
|
|
38
|
+
if (this.skipping && skipActive) return result
|
|
39
|
+
const currIncReplacement = this.includeReplacements.$current()
|
|
40
|
+
const drop = currIncReplacement.drop || (currIncReplacement.drop = [])
|
|
41
|
+
directiveLineno -= currIncReplacement.offset || 0
|
|
42
|
+
const depthChange = this.conditional_stack.length - depth
|
|
43
|
+
if (depthChange < 0) {
|
|
44
|
+
if (skipActive) {
|
|
45
|
+
for (let n = drop.pop(); n <= directiveLineno; n++) drop.push(n)
|
|
46
|
+
} else {
|
|
47
|
+
drop.push(directiveLineno)
|
|
48
|
+
}
|
|
49
|
+
} else if (depthChange > 0 || directiveLineno === this.lineno) {
|
|
50
|
+
drop.push(directiveLineno)
|
|
51
|
+
} else {
|
|
52
|
+
drop.push([directiveLineno, text])
|
|
53
|
+
}
|
|
54
|
+
return result
|
|
55
|
+
}
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
return scope
|
|
59
|
+
})()
|
|
60
|
+
|
|
61
|
+
const CurrentPosition = (() => {
|
|
62
|
+
const parentScope = Opal.module(Opal.Asciidoctor, 'Reducer', $Reducer)
|
|
63
|
+
const scope = Opal.module(parentScope, 'CurrentPosition', function $CurrentPosition () {})
|
|
64
|
+
|
|
65
|
+
Opal.defn(Opal.get_singleton_class(scope), '$extended', function $extended (instance) {
|
|
66
|
+
instance.$to_end()
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
Opal.defn(scope, '$current', function $current () {
|
|
70
|
+
return this[this.pointer]
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
Opal.defn(scope, '$to_end', function $toEnd () {
|
|
74
|
+
this.pointer = this.length - 1
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
Opal.defn(scope, '$up', function $up () {
|
|
78
|
+
this.pointer = this.$current().into
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
return scope
|
|
82
|
+
})()
|
|
83
|
+
|
|
84
|
+
const IncludeDirectiveTracker = (() => {
|
|
85
|
+
const parentScope = Opal.module(Opal.Asciidoctor, 'Reducer', $Reducer)
|
|
86
|
+
const scope = Opal.module(parentScope, 'IncludeDirectiveTracker', function $IncludeDirectiveTracker () {})
|
|
87
|
+
|
|
88
|
+
Opal.defn(Opal.get_singleton_class(scope), '$extended', function $extended (instance) {
|
|
89
|
+
instance.includeReplacements = [{}].$extend(CurrentPosition)
|
|
90
|
+
instance.$$reducer = {}
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
Opal.defn(scope, '$preprocess_include_directive', function preprocessIncludeDirective (target, attrlist) {
|
|
94
|
+
this.$$reducer.includeDirectiveLine = `include::${target}[${attrlist}]`
|
|
95
|
+
this.$$reducer.includePushed = false
|
|
96
|
+
const directiveLineno = this.lineno // we're currently on the include line, which is 1-based
|
|
97
|
+
const result = Opal.send(
|
|
98
|
+
this,
|
|
99
|
+
Opal.find_super_dispatcher(this, 'preprocess_include_directive', preprocessIncludeDirective),
|
|
100
|
+
[target, attrlist]
|
|
101
|
+
)
|
|
102
|
+
if (!this.$$reducer.includePushed) {
|
|
103
|
+
let ln = this.peekLine(true)
|
|
104
|
+
let unresolved
|
|
105
|
+
if (
|
|
106
|
+
ln &&
|
|
107
|
+
ln.charAt(ln.length - 1) === ']' &&
|
|
108
|
+
!(unresolved = ln.startsWith('Unresolved directive in ')) &&
|
|
109
|
+
directiveLineno === this.lineno &&
|
|
110
|
+
(unresolved = ln.startsWith('link:'))
|
|
111
|
+
) {
|
|
112
|
+
ln = `${ln.slice(0, ln.length - 1)}role=include]`
|
|
113
|
+
}
|
|
114
|
+
pushIncludeReplacement.call(this, directiveLineno, unresolved ? [ln] : [], 0, unresolved)
|
|
115
|
+
}
|
|
116
|
+
this.$$reducer = {}
|
|
117
|
+
return result
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
Opal.defn(scope, '$push_include', function pushInclude (data, file, path, lineno, attrs) {
|
|
121
|
+
this.$$reducer.includePushed = true
|
|
122
|
+
const directiveLineno = this.lineno - 1 // we're below the include line, which is 1-based
|
|
123
|
+
const prevIncDepth = this.include_stack.length
|
|
124
|
+
const result = Opal.send(this, Opal.find_super_dispatcher(this, 'push_include', pushInclude), [
|
|
125
|
+
data,
|
|
126
|
+
file,
|
|
127
|
+
path,
|
|
128
|
+
lineno,
|
|
129
|
+
attrs,
|
|
130
|
+
])
|
|
131
|
+
pushIncludeReplacement.call(
|
|
132
|
+
this,
|
|
133
|
+
directiveLineno,
|
|
134
|
+
this.include_stack.length > prevIncDepth ? this.$lines() : [],
|
|
135
|
+
lineno > 1 ? lineno - 1 : 0
|
|
136
|
+
)
|
|
137
|
+
return result
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
Opal.defn(scope, '$pop_include', function popInclude () {
|
|
141
|
+
if (!this.$$reducer.includePushed) this.includeReplacements.$up()
|
|
142
|
+
return Opal.send(this, Opal.find_super_dispatcher(this, 'pop_include', popInclude), [])
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
function pushIncludeReplacement (lineno, lines, offset, unresolved) {
|
|
146
|
+
const incReplacements = this.includeReplacements
|
|
147
|
+
const into = incReplacements.pointer
|
|
148
|
+
const line = this.$$reducer.includeDirectiveLine
|
|
149
|
+
incReplacements.push({ into, lineno: lineno - (incReplacements.$current().offset || 0), line, lines, offset })
|
|
150
|
+
if (!unresolved && lines.length) incReplacements.$to_end()
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return scope
|
|
154
|
+
})()
|
|
155
|
+
|
|
156
|
+
function preprocessor () {
|
|
157
|
+
this.process((doc, reader) =>
|
|
158
|
+
doc.getOptions().preserve_conditionals
|
|
159
|
+
? reader.$extend(IncludeDirectiveTracker)
|
|
160
|
+
: reader.$extend(ConditionalDirectiveTracker, IncludeDirectiveTracker)
|
|
161
|
+
)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function treeProcessor () {
|
|
165
|
+
this.process((doc) => {
|
|
166
|
+
const incReplacements = doc.reader.includeReplacements
|
|
167
|
+
if (incReplacements.length > 1 || (incReplacements[0].drop || []).length) {
|
|
168
|
+
const sourceLines = doc.getSourceLines()
|
|
169
|
+
incReplacements[0].lines = sourceLines.slice()
|
|
170
|
+
incReplacements
|
|
171
|
+
.slice()
|
|
172
|
+
.reverse()
|
|
173
|
+
.forEach(({ into, lineno, lines, line, drop }) => {
|
|
174
|
+
let targetLines, idx
|
|
175
|
+
if (into != null) {
|
|
176
|
+
targetLines = incReplacements[into].lines
|
|
177
|
+
// adds assurance that we're replacing the correct line
|
|
178
|
+
if (targetLines[(idx = lineno - 1)] !== line) return
|
|
179
|
+
}
|
|
180
|
+
if ((drop || []).length) {
|
|
181
|
+
drop
|
|
182
|
+
.slice()
|
|
183
|
+
.reverse()
|
|
184
|
+
.forEach((dropIt) => {
|
|
185
|
+
Array.isArray(dropIt) ? (lines[dropIt[0] - 1] = dropIt[1]) : lines.splice(dropIt - 1, 1)
|
|
186
|
+
})
|
|
187
|
+
}
|
|
188
|
+
if (targetLines) targetLines[idx] = lines
|
|
189
|
+
})
|
|
190
|
+
const reducedSourceLines = flattenDeep(incReplacements[0].lines)
|
|
191
|
+
if (doc.getSourcemap()) {
|
|
192
|
+
const logger = Asciidoctor.LoggerManager.getLogger()
|
|
193
|
+
const opts = Object.assign(doc.getOptions(), { logger: undefined, parse: false, reduced: true })
|
|
194
|
+
if (opts.extension_registry) {
|
|
195
|
+
opts.extension_registry = Asciidoctor.Extensions.Registry.$new(opts.extension_registry.groups)
|
|
196
|
+
}
|
|
197
|
+
const includes = doc.getCatalog().includes
|
|
198
|
+
doc = Asciidoctor.load(reducedSourceLines, opts)
|
|
199
|
+
doc.catalog.$send('[]=', 'includes', includes)
|
|
200
|
+
doc.parse()
|
|
201
|
+
Asciidoctor.LoggerManager.setLogger(logger)
|
|
202
|
+
} else {
|
|
203
|
+
while (reducedSourceLines[reducedSourceLines.length - 1] === '') reducedSourceLines.pop()
|
|
204
|
+
sourceLines.splice(0, sourceLines.length, ...reducedSourceLines)
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return doc
|
|
208
|
+
})
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function flattenDeep (array, accum = []) {
|
|
212
|
+
const len = array.length
|
|
213
|
+
for (let i = 0, it; i < len; i++) Array.isArray((it = array[i])) ? flattenDeep(it, accum) : accum.push(it)
|
|
214
|
+
return accum
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function toProc (fn) {
|
|
218
|
+
return Object.defineProperty(fn, '$$arity', { value: fn.length })
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
module.exports.register = (registry) => {
|
|
222
|
+
const extGroup = toProc(function () {
|
|
223
|
+
const doc = this.document
|
|
224
|
+
doc.$extend(DocumentExt)
|
|
225
|
+
if (doc.getOptions().reduced) return
|
|
226
|
+
this.preprocessor(preprocessor)
|
|
227
|
+
this.treeProcessor(treeProcessor)
|
|
228
|
+
})
|
|
229
|
+
registry.groups.$send('[]=', 'reducer', extGroup)
|
|
230
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const loadConfig = require('./load-config')
|
|
4
|
+
const produceAggregateDocuments = require('./produce-aggregate-documents')
|
|
5
|
+
const PromiseQueue = require('./util/promise-queue')
|
|
6
|
+
|
|
7
|
+
async function assembleContent (playbook, contentCatalog, converter, { siteCatalog, configSource }) {
|
|
8
|
+
// Q: could we get ContentCatalog#getComponentVersionStartPage() in Antora core?
|
|
9
|
+
if (typeof contentCatalog.getComponentVersionStartPage !== 'function') {
|
|
10
|
+
contentCatalog.getComponentVersionStartPage = function (component, version) {
|
|
11
|
+
return this.resolvePage('index.adoc', { component, version })
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
const assemblerConfig = await loadConfig(playbook, configSource)
|
|
15
|
+
if (!assemblerConfig) return [] // TODO consider removing and doing this another way
|
|
16
|
+
const generatorFunctions = this ? this.getFunctions() : {}
|
|
17
|
+
const { loadAsciiDoc = require('@antora/asciidoc-loader') } = generatorFunctions
|
|
18
|
+
const aggregateDocuments = produceAggregateDocuments(loadAsciiDoc, contentCatalog, assemblerConfig)
|
|
19
|
+
if (!converter) return aggregateDocuments
|
|
20
|
+
const { publishSite: publishFiles = require('@antora/site-publisher') } = generatorFunctions
|
|
21
|
+
const buildConfig = assemblerConfig.build
|
|
22
|
+
await prepareWorkspace(publishFiles, aggregateDocuments, contentCatalog, buildConfig)
|
|
23
|
+
// TODO: pass more information to converter so it doesn't have to compute internal stuff
|
|
24
|
+
// Q: don't we need to pass in the combined/resolved AsciiDoc attributes per file or component version?
|
|
25
|
+
return new PromiseQueue({ concurrency: buildConfig.processLimit })
|
|
26
|
+
.add(aggregateDocuments.map((doc) => () => converter.call(this, doc, buildConfig)))
|
|
27
|
+
.toPromise()
|
|
28
|
+
.then((files) => {
|
|
29
|
+
if (buildConfig.publish && siteCatalog) siteCatalog.addFiles(files)
|
|
30
|
+
return files
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// TODO: if no workspace dir is defined; we shouldn't continue
|
|
35
|
+
function prepareWorkspace (publishFiles, aggregateDocuments, contentCatalog, buildConfig) {
|
|
36
|
+
const { dir, clean, keepAggregateSource } = buildConfig
|
|
37
|
+
const files = contentCatalog.findBy({ family: 'image' }).filter(({ out }) => out?.assembled)
|
|
38
|
+
if (keepAggregateSource) {
|
|
39
|
+
files.push(...aggregateDocuments.map((file) => Object.assign(file, { out: { path: file.path } })))
|
|
40
|
+
}
|
|
41
|
+
// TODO: site publisher should accept a single catalog
|
|
42
|
+
return publishFiles({ output: { clean, dir } }, [{ getFiles: () => files }])
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
module.exports = assembleContent
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { compile: bracesToGroup } = require('braces')
|
|
4
|
+
const { makeRe: makePicomatchRx } = require('picomatch')
|
|
5
|
+
|
|
6
|
+
const PICOMATCH_OPTS = {
|
|
7
|
+
bash: true,
|
|
8
|
+
expandRange: (begin, end, step, opts) => bracesToGroup(opts ? `{${begin}..${end}..${step}}` : `{${begin}..${end}}`),
|
|
9
|
+
fastpaths: false,
|
|
10
|
+
nobracket: true,
|
|
11
|
+
noglobstar: true,
|
|
12
|
+
nonegate: true,
|
|
13
|
+
noquantifiers: true,
|
|
14
|
+
regex: false,
|
|
15
|
+
strictSlashes: true,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const VERSION_SEPARATOR_RX = /@(?!\()/
|
|
19
|
+
|
|
20
|
+
function compilePatterns (patterns) {
|
|
21
|
+
if (patterns[0].charAt() === '!') patterns = ['**', ...patterns]
|
|
22
|
+
return patterns.map((pattern) => {
|
|
23
|
+
const negated = pattern.charAt() === '!'
|
|
24
|
+
if (negated) pattern = pattern.substr(1)
|
|
25
|
+
let version
|
|
26
|
+
const separatorIdx = pattern.search(VERSION_SEPARATOR_RX)
|
|
27
|
+
if (~separatorIdx) {
|
|
28
|
+
pattern = (version = true) && `${pattern.substr(0, separatorIdx)}%${pattern.substr(separatorIdx + 1) || '*'}`
|
|
29
|
+
}
|
|
30
|
+
return Object.assign(makePicomatchRx(pattern, PICOMATCH_OPTS), {
|
|
31
|
+
globstar: pattern === '**',
|
|
32
|
+
negated,
|
|
33
|
+
star: pattern === '*',
|
|
34
|
+
version,
|
|
35
|
+
})
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function filterComponentVersions (components, patterns) {
|
|
40
|
+
if (!patterns.length) return []
|
|
41
|
+
const rxs = compilePatterns(patterns)
|
|
42
|
+
return components.reduce((accum, { latest, versions }) => {
|
|
43
|
+
accum.push(
|
|
44
|
+
...versions.filter((version) => {
|
|
45
|
+
let matched
|
|
46
|
+
for (const rx of rxs) {
|
|
47
|
+
let voteIfMatched
|
|
48
|
+
if (matched) {
|
|
49
|
+
if (rx.negated) voteIfMatched = false
|
|
50
|
+
} else if (!rx.negated) {
|
|
51
|
+
voteIfMatched = true
|
|
52
|
+
}
|
|
53
|
+
if (voteIfMatched == null) continue
|
|
54
|
+
if (rx.globstar) {
|
|
55
|
+
matched = voteIfMatched
|
|
56
|
+
} else if (rx.star) {
|
|
57
|
+
if (version === latest) matched = voteIfMatched
|
|
58
|
+
} else if (rx.version) {
|
|
59
|
+
if (rx.test(`${version.version}%${version.name}`)) matched = voteIfMatched
|
|
60
|
+
} else if (version === latest && rx.test(version.name)) {
|
|
61
|
+
matched = voteIfMatched
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return matched
|
|
65
|
+
})
|
|
66
|
+
)
|
|
67
|
+
return accum
|
|
68
|
+
}, [])
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
module.exports = filterComponentVersions
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const camelCaseKeys = require('camelcase-keys')
|
|
4
|
+
const expandPath = require('@antora/expand-path-helper')
|
|
5
|
+
const { promises: fsp } = require('fs')
|
|
6
|
+
const os = require('os')
|
|
7
|
+
const yaml = require('js-yaml')
|
|
8
|
+
|
|
9
|
+
const CAMEL_CASE_KEYS_OPTS = { deep: true, stopPaths: ['asciidoc'] }
|
|
10
|
+
|
|
11
|
+
function loadConfig (playbook, configSource = './antora-assembler.yml') {
|
|
12
|
+
return (
|
|
13
|
+
configSource.constructor === Object
|
|
14
|
+
? Promise.resolve(configSource)
|
|
15
|
+
: fsp
|
|
16
|
+
.access((configSource = expandPath(configSource, { dot: playbook.dir })))
|
|
17
|
+
.then(
|
|
18
|
+
() => true,
|
|
19
|
+
() => false
|
|
20
|
+
)
|
|
21
|
+
.then((exists) =>
|
|
22
|
+
exists
|
|
23
|
+
? fsp.readFile(configSource).then((data) => camelCaseKeys(yaml.load(data), CAMEL_CASE_KEYS_OPTS))
|
|
24
|
+
: {}
|
|
25
|
+
)
|
|
26
|
+
).then((config) => {
|
|
27
|
+
if (config.enabled === false) return undefined
|
|
28
|
+
let asciidocAttrs
|
|
29
|
+
if (!config.asciidoc) {
|
|
30
|
+
config.asciidoc = { attributes: (asciidocAttrs = { doctype: 'book' }) }
|
|
31
|
+
} else if (!(asciidocAttrs = config.asciidoc.attributes)) {
|
|
32
|
+
config.asciidoc.attributes = asciidocAttrs = { doctype: 'book' }
|
|
33
|
+
}
|
|
34
|
+
Object.assign(asciidocAttrs, { revdate: new Date().toISOString().split('T')[0], 'page-partial': null })
|
|
35
|
+
if (config.componentVersions == null) {
|
|
36
|
+
config.componentVersions = ['**']
|
|
37
|
+
} else if (typeof config.componentVersions === 'string') {
|
|
38
|
+
config.componentVersions = config.componentVersions.split(', ')
|
|
39
|
+
}
|
|
40
|
+
if (!('rootLevel' in config)) config.rootLevel = 0
|
|
41
|
+
if (!('insertStartPage' in config)) config.insertStartPage = true
|
|
42
|
+
if (['discrete', 'fuse', 'enclose'].indexOf(config.sectionMergeStrategy) < 0) {
|
|
43
|
+
config.sectionMergeStrategy = 'discrete'
|
|
44
|
+
}
|
|
45
|
+
const build = config.build || (config.build = {})
|
|
46
|
+
if (build.dir === '$' + '{playbook.output.dir}') {
|
|
47
|
+
//build.dir = playbook.output.dir
|
|
48
|
+
throw new Error('Not implemented')
|
|
49
|
+
} else {
|
|
50
|
+
build.dir = expandPath(build.dir || './build/assembler', { dot: playbook.dir })
|
|
51
|
+
}
|
|
52
|
+
build.cwd = playbook.dir // use playbook.dir the purpose of finding and loading require scripts
|
|
53
|
+
if (!('clean' in build) && 'output' in playbook) build.clean = playbook.output.clean
|
|
54
|
+
if (!('publish' in build)) build.publish = true
|
|
55
|
+
if (!build.processLimit) {
|
|
56
|
+
build.processLimit = 'processLimit' in build ? Infinity : Math.round(os.cpus().length * 0.5)
|
|
57
|
+
}
|
|
58
|
+
return config
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
module.exports = loadConfig
|
|
@@ -0,0 +1,520 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const File = require('vinyl')
|
|
4
|
+
const { posix: path } = require('path')
|
|
5
|
+
|
|
6
|
+
function produceAggregateDocument (
|
|
7
|
+
loadAsciiDoc,
|
|
8
|
+
contentCatalog,
|
|
9
|
+
componentVersion,
|
|
10
|
+
outline,
|
|
11
|
+
pages,
|
|
12
|
+
asciidocConfig,
|
|
13
|
+
mutableAttributes,
|
|
14
|
+
sectionMergeStrategy = 'discrete'
|
|
15
|
+
) {
|
|
16
|
+
const pagesInOutline = selectPagesInOutline(outline, pages)
|
|
17
|
+
const navtitle = outline.content
|
|
18
|
+
const stem = generateStem(componentVersion, navtitle)
|
|
19
|
+
const header = buildAsciiDocHeader(componentVersion, navtitle)
|
|
20
|
+
const body = aggregateAsciiDoc(
|
|
21
|
+
loadAsciiDoc,
|
|
22
|
+
contentCatalog,
|
|
23
|
+
header,
|
|
24
|
+
componentVersion,
|
|
25
|
+
outline,
|
|
26
|
+
pagesInOutline,
|
|
27
|
+
asciidocConfig,
|
|
28
|
+
mutableAttributes,
|
|
29
|
+
sectionMergeStrategy
|
|
30
|
+
)
|
|
31
|
+
const relativeSrcPath = `${stem}.adoc`
|
|
32
|
+
return new File({
|
|
33
|
+
asciidoc: asciidocConfig,
|
|
34
|
+
contents: Buffer.from([...header, ...body].join('\n') + '\n'),
|
|
35
|
+
mediaType: 'text/asciidoc',
|
|
36
|
+
path: relativeSrcPath,
|
|
37
|
+
src: {
|
|
38
|
+
component: componentVersion.name,
|
|
39
|
+
version: componentVersion.version,
|
|
40
|
+
basename: path.basename(relativeSrcPath),
|
|
41
|
+
stem,
|
|
42
|
+
extname: '.adoc',
|
|
43
|
+
},
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function buildAsciiDocHeader (componentVersion, navtitle) {
|
|
48
|
+
const doctitle = navtitle === componentVersion.title ? navtitle : `${componentVersion.title}: ${navtitle}`
|
|
49
|
+
const version = componentVersion.version && componentVersion.version !== 'master' ? componentVersion.version : ''
|
|
50
|
+
return [
|
|
51
|
+
`= ${doctitle}`,
|
|
52
|
+
...(version ? [`v${version}`] : []),
|
|
53
|
+
':doctype: book', // for debugging only; set via CLI
|
|
54
|
+
// Q: should we pass these via the CLI so they cannot be modified?
|
|
55
|
+
`:page-component-name: ${componentVersion.name}`,
|
|
56
|
+
`:page-component-version:${version ? ' ' + version : ''}`,
|
|
57
|
+
':page-version: {page-component-version}',
|
|
58
|
+
`:page-component-display-version: ${componentVersion.displayVersion}`,
|
|
59
|
+
`:page-component-title: ${componentVersion.title}`,
|
|
60
|
+
]
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function selectPagesInOutline (outlineEntry, pages) {
|
|
64
|
+
const page = outlineEntry.urlType === 'internal' ? pages.find((it) => it.pub.url === outlineEntry.url) : undefined
|
|
65
|
+
return (outlineEntry.items || []).reduce(
|
|
66
|
+
(accum, item) => new Map([...accum, ...selectPagesInOutline(item, pages)]),
|
|
67
|
+
new Map(
|
|
68
|
+
page && [
|
|
69
|
+
[`${page.src.module === 'ROOT' ? '' : page.src.module + ':'}${page.src.relative}`, page],
|
|
70
|
+
[page.pub.url, page],
|
|
71
|
+
]
|
|
72
|
+
)
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function aggregateAsciiDoc (
|
|
77
|
+
loadAsciiDoc,
|
|
78
|
+
contentCatalog,
|
|
79
|
+
header,
|
|
80
|
+
componentVersion,
|
|
81
|
+
outlineEntry,
|
|
82
|
+
pagesInOutline,
|
|
83
|
+
asciidocConfig,
|
|
84
|
+
mutableAttributes,
|
|
85
|
+
sectionMergeStrategy,
|
|
86
|
+
level = 0
|
|
87
|
+
) {
|
|
88
|
+
const buffer = []
|
|
89
|
+
// TODO: we could try to be smart about it and make sure the page with fragment is included at least once
|
|
90
|
+
if (outlineEntry.hash) return buffer
|
|
91
|
+
const { content: navtitle, items, unresolved, urlType, url } = outlineEntry
|
|
92
|
+
// FIXME: ideally, resource ID would be stored in navigation so we can look up the page more efficiently
|
|
93
|
+
let page = urlType === 'internal' && !unresolved ? pagesInOutline.get(url) : undefined
|
|
94
|
+
if (page) {
|
|
95
|
+
let contents = page.src.contents
|
|
96
|
+
// NOTE: blank lines at top and bottom of document create mismatch when using line numbers to navigate source lines
|
|
97
|
+
// IMPORTANT: this must not leave behind lines the parser will drop!
|
|
98
|
+
// IDEA: another option is to capture initial lineno of reader and use as offset (but preseves those blank lines)
|
|
99
|
+
contents = Buffer.from(
|
|
100
|
+
contents
|
|
101
|
+
.toString()
|
|
102
|
+
.replace(/^(?:[ \t]*\r\n?|[ \t]*\n)+/, '')
|
|
103
|
+
.trimRight()
|
|
104
|
+
)
|
|
105
|
+
page = new page.constructor(Object.assign({}, page, { contents, mediaType: 'text/asciidoc' }))
|
|
106
|
+
const { module: module_, relative, origin } = page.src
|
|
107
|
+
const doc = loadAsciiDoc(page, contentCatalog, asciidocConfig)
|
|
108
|
+
const ids = doc.getCatalog().ids
|
|
109
|
+
// NOTE: in Antora, docname is relative src path from module without file extension
|
|
110
|
+
const docname = doc.getAttribute('docname')
|
|
111
|
+
//const idprefix = `${module_ === 'ROOT' ? '' : module_ + ':'}${docname}//`
|
|
112
|
+
const idprefix = `${module_ === 'ROOT' ? '' : module_ + ':'}${docname.replace(/[/]/g, '::')}:::`
|
|
113
|
+
//const idprefix = `${module_ === 'ROOT' ? '' : module_ + ':'}${docname.replace(/[/]/g, ':-:')}:--:`
|
|
114
|
+
buffer.push('')
|
|
115
|
+
buffer.push(`:docname: ${docname}`)
|
|
116
|
+
buffer.push(`:page-module: ${module_}`)
|
|
117
|
+
buffer.push(`:page-relative-src-path: ${relative}`)
|
|
118
|
+
//buffer.push(`:page-origin-type: ${origin.type}`)
|
|
119
|
+
buffer.push(`:page-origin-url: ${origin.url}`)
|
|
120
|
+
buffer.push(`:page-origin-start-path:${origin.startPath && ' '}${origin.startPath}`)
|
|
121
|
+
buffer.push(`:page-origin-refname: ${origin.branch || origin.tag}`)
|
|
122
|
+
buffer.push(`:page-origin-reftype: ${origin.branch ? 'branch' : 'tag'}`)
|
|
123
|
+
buffer.push(`:page-origin-refhash: ${origin.worktree ? '(worktree)' : origin.refhash}`)
|
|
124
|
+
let enclosed
|
|
125
|
+
// NOTE: if level is 0, doctitle has already been added and we're in the document header
|
|
126
|
+
if (level) {
|
|
127
|
+
if (level === 1 && navtitle === componentVersion.title) {
|
|
128
|
+
level--
|
|
129
|
+
} else {
|
|
130
|
+
let hlevel = level + 1
|
|
131
|
+
if (hlevel > 6) {
|
|
132
|
+
hlevel = 6
|
|
133
|
+
buffer.push(`[discrete#${idprefix}]`)
|
|
134
|
+
} else {
|
|
135
|
+
buffer.push(`[#${idprefix}]`)
|
|
136
|
+
}
|
|
137
|
+
buffer.push(`${'='.repeat(hlevel)} ${navtitle}`)
|
|
138
|
+
}
|
|
139
|
+
} else {
|
|
140
|
+
header.unshift(`[#${idprefix}]`)
|
|
141
|
+
}
|
|
142
|
+
if (sectionMergeStrategy === 'enclose' && items && doc.hasSections()) {
|
|
143
|
+
enclosed = true
|
|
144
|
+
// TODO: make overview section title configurable
|
|
145
|
+
//let overviewTitle = doc.getDocumentTitle()
|
|
146
|
+
//if (overviewTitle === navtitle) overviewTitle = doc.getAttribute('overview-title', 'Overview')
|
|
147
|
+
const overviewTitle = doc.getAttribute('overview-title', 'Overview')
|
|
148
|
+
buffer.push('')
|
|
149
|
+
// NOTE: try to toggle sectids; otherwise, fallback to globally unique synthetic ID
|
|
150
|
+
let toggleSectids, syntheticId
|
|
151
|
+
if (doc.isAttribute('sectids')) {
|
|
152
|
+
if (doc.isAttributeLocked('sectids')) {
|
|
153
|
+
syntheticId = `__object-id-${getObjectId(outlineEntry)}`
|
|
154
|
+
} else {
|
|
155
|
+
buffer.push(':!sectids:')
|
|
156
|
+
toggleSectids = true
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
let hlevel = level + 2
|
|
160
|
+
if (hlevel > 6) {
|
|
161
|
+
hlevel = 6
|
|
162
|
+
buffer.push(syntheticId ? `[discrete#${syntheticId}]` : '[discrete]')
|
|
163
|
+
} else if (syntheticId) {
|
|
164
|
+
buffer.push(`[#${syntheticId}]`)
|
|
165
|
+
}
|
|
166
|
+
buffer.push(`${'='.repeat(hlevel)} ${overviewTitle}`)
|
|
167
|
+
if (toggleSectids) buffer.push(':sectids:')
|
|
168
|
+
}
|
|
169
|
+
const siteUrl = doc.getAttribute('site-url')
|
|
170
|
+
const lines = doc.getSourceLines()
|
|
171
|
+
const ignoreLines = []
|
|
172
|
+
// TODO: think more about when multipart is allowed; perhaps configurable
|
|
173
|
+
if (doc.hasSections()) fixSectionLevels(doc.getSections(), level === 0)
|
|
174
|
+
// TODO: update / simplify findBy calls when upgrading to Asciidoctor 2
|
|
175
|
+
const allBlocks = doc
|
|
176
|
+
.findBy((it) => it.getContext() !== 'document')
|
|
177
|
+
.reduce((accum, block) => {
|
|
178
|
+
accum.push(block)
|
|
179
|
+
if (block.getContext() === 'table') {
|
|
180
|
+
const rows = block.rows
|
|
181
|
+
;[...rows.body, ...rows.foot].forEach((row) => {
|
|
182
|
+
row.forEach((cell) => {
|
|
183
|
+
if (cell.style !== 'asciidoc') return
|
|
184
|
+
accum.push(...cell.inner_document.findBy((it) => it.getContext() !== 'document'))
|
|
185
|
+
})
|
|
186
|
+
})
|
|
187
|
+
}
|
|
188
|
+
return accum
|
|
189
|
+
}, [])
|
|
190
|
+
allBlocks.forEach((block) => {
|
|
191
|
+
const contentModel = block.content_model
|
|
192
|
+
if (
|
|
193
|
+
((contentModel === 'verbatim' && block.getContext() !== 'table_cell') ||
|
|
194
|
+
contentModel === 'simple' ||
|
|
195
|
+
contentModel === 'pass') &&
|
|
196
|
+
!block.hasSubstitution('macros')
|
|
197
|
+
) {
|
|
198
|
+
const lineno = block.getLineNumber()
|
|
199
|
+
const idx = typeof lineno === 'number' ? lineno - 1 : undefined
|
|
200
|
+
const startLine = lines[idx]
|
|
201
|
+
// NOTE: one case this happens if when sourcemap isn't enabled when reducing
|
|
202
|
+
if (startLine == null) {
|
|
203
|
+
console.log(`null startLine for ${block.getContext()} at ${lineno} in ${relative}`)
|
|
204
|
+
return
|
|
205
|
+
}
|
|
206
|
+
const char0 = startLine.charAt()
|
|
207
|
+
// FIXME: needs to be more robust; move logic to helper
|
|
208
|
+
const delimited =
|
|
209
|
+
startLine.length > 3 &&
|
|
210
|
+
startLine === char0.repeat(startLine.length) &&
|
|
211
|
+
(char0 === '-' || char0 === '.' || char0 === '+')
|
|
212
|
+
// QUESTION: exclude block attribute lines too? what about attribute entries?
|
|
213
|
+
for (let i = idx; i < block.lines.length + (delimited ? idx + 2 : idx); i++) ignoreLines.push(i)
|
|
214
|
+
}
|
|
215
|
+
})
|
|
216
|
+
for (let idx = 0, len = lines.length; idx < len; idx++) {
|
|
217
|
+
if (~ignoreLines.indexOf(idx)) continue
|
|
218
|
+
let line = lines[idx]
|
|
219
|
+
if (~line.indexOf('<<')) {
|
|
220
|
+
line = line.replace(/(?<![\\+])<<#?([\p{Alpha}0-9_/.:{][^>,]*?)(?:|, *([^>]+?))?>>/gu, (m, refid, text) => {
|
|
221
|
+
// support natural xref; note this logic will change when upgrading to Asciidoctor 2
|
|
222
|
+
if (!ids['$key?'](refid) && (~refid.indexOf(' ') || refid.toLowerCase() !== refid)) {
|
|
223
|
+
if (!(refid = doc.getCatalog().ids.$key(refid).$to_s())) {
|
|
224
|
+
return m
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return `<<${idprefix}${refid}${text ? ',' + text : ''}>>`
|
|
228
|
+
})
|
|
229
|
+
}
|
|
230
|
+
// NOTE: the next check takes care of inline and block anchors
|
|
231
|
+
if (~line.indexOf('[[')) {
|
|
232
|
+
line = line.replace(/\[\[([\p{Alpha}_:][\p{Alpha}0-9_\-:.]*)(|, *.+?)\]\]/gu, `[[${idprefix}$1$2]]`)
|
|
233
|
+
}
|
|
234
|
+
if (~line.indexOf('xref:')) {
|
|
235
|
+
// Q: should we allow : as first character of target?
|
|
236
|
+
line = line.replace(/xref:([\p{Alpha}#/.{].*?)\[(|.*?[^\\])\]/gu, (m, target, text) => {
|
|
237
|
+
let pagePart, fragment, targetPage
|
|
238
|
+
const hashIdx = target.indexOf('#')
|
|
239
|
+
if (~hashIdx) {
|
|
240
|
+
pagePart = target.substr(0, hashIdx)
|
|
241
|
+
fragment = target.substr(hashIdx + 1)
|
|
242
|
+
// TODO: for now, assume .adoc; in the future, consider other file extensions
|
|
243
|
+
if (!(pagePart && pagePart.endsWith('.adoc'))) pagePart += '.adoc'
|
|
244
|
+
} else if (target.endsWith('.adoc')) {
|
|
245
|
+
pagePart = target
|
|
246
|
+
fragment = ''
|
|
247
|
+
} else {
|
|
248
|
+
fragment = target
|
|
249
|
+
}
|
|
250
|
+
if (!pagePart) return `<<${idprefix}${fragment}${text ? ',' + text.replace(/\\]/g, ']') : ''}>>`
|
|
251
|
+
if (~pagePart.indexOf('@') || /:.*:/.test(pagePart)) {
|
|
252
|
+
// TODO: handle unresolved page better
|
|
253
|
+
return siteUrl && (targetPage = contentCatalog.resolvePage(pagePart, page.src))
|
|
254
|
+
? `${siteUrl}${targetPage.pub.url}${fragment && '#' + fragment}[${text}]`
|
|
255
|
+
: m
|
|
256
|
+
} else if (pagePart.indexOf(':') < 0) {
|
|
257
|
+
if (module_ !== 'ROOT') pagePart = `${module_}:${pagePart}`
|
|
258
|
+
} else if (pagePart.startsWith('ROOT:')) {
|
|
259
|
+
pagePart = pagePart.substr(5)
|
|
260
|
+
}
|
|
261
|
+
if (!(targetPage = pagesInOutline.get(pagePart))) {
|
|
262
|
+
// TODO: handle unresolved page better
|
|
263
|
+
return siteUrl && (targetPage = contentCatalog.resolvePage(target, page.src))
|
|
264
|
+
? `${siteUrl}${targetPage.pub.url}${fragment && '#' + fragment}[${text}]`
|
|
265
|
+
: m
|
|
266
|
+
}
|
|
267
|
+
pagePart = pagePart.replace(/[/]/g, '::').replace(/\.adoc$/, '')
|
|
268
|
+
const refid = `${pagePart}:::${fragment}`
|
|
269
|
+
return `<<${refid}${text && text !== targetPage.title ? ',' + text.replace(/\\]/g, ']') : ''}>>`
|
|
270
|
+
})
|
|
271
|
+
}
|
|
272
|
+
if (~line.indexOf('link:{attachmentsdir}/')) {
|
|
273
|
+
line = line.replace(/(?<![\\+])link:\{attachmentsdir\}\/([^\s[]+)\[(|.*?[^\\])\]/g, (m, relative, text) => {
|
|
274
|
+
const attachment =
|
|
275
|
+
siteUrl &&
|
|
276
|
+
contentCatalog.getById({
|
|
277
|
+
component: componentVersion.name,
|
|
278
|
+
version: componentVersion.version,
|
|
279
|
+
module: module_,
|
|
280
|
+
family: 'attachment',
|
|
281
|
+
relative,
|
|
282
|
+
})
|
|
283
|
+
return attachment ? `${siteUrl}${attachment.pub.url}[${text}]` : m
|
|
284
|
+
})
|
|
285
|
+
}
|
|
286
|
+
if (~line.indexOf('image:') && !line.startsWith('image::')) {
|
|
287
|
+
line = line.replace(/(?<![\\+])image:([^:\s[](?:[^[]*[^\s[])?)\[([^\]]*)\]/g, (m, target, attrlist) => {
|
|
288
|
+
if (isResourceSpec(target)) {
|
|
289
|
+
const image = contentCatalog.resolveResource(target, page.src, 'image', ['image'])
|
|
290
|
+
// TODO: handle (or report) unresolved image better
|
|
291
|
+
if (image) {
|
|
292
|
+
image.out.assembled = true
|
|
293
|
+
return `image:${image.pub.url.substr(1)}[${attrlist}]`
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return m
|
|
297
|
+
})
|
|
298
|
+
}
|
|
299
|
+
lines[idx] = line
|
|
300
|
+
}
|
|
301
|
+
// NOTE: need to do this last since it modifies the line numbers
|
|
302
|
+
// we could remap the line numbers to make them resilient
|
|
303
|
+
// or we could mark which lines to remove and filter them after
|
|
304
|
+
;[...allBlocks].reverse().forEach((block) => {
|
|
305
|
+
const lineno = block.getLineNumber()
|
|
306
|
+
// NOTE: lineno is not defined for preamble
|
|
307
|
+
if (typeof lineno !== 'number') return
|
|
308
|
+
const context = block.getContext()
|
|
309
|
+
const idx = lineno - 1
|
|
310
|
+
if (context === 'section') {
|
|
311
|
+
if (block.getSectionName() === 'header') {
|
|
312
|
+
lines.splice(idx, 1)
|
|
313
|
+
return
|
|
314
|
+
}
|
|
315
|
+
let blockStyle = sectionMergeStrategy === 'discrete' ? 'discrete' : undefined
|
|
316
|
+
// FIXME: quick fix; needs more thorough review
|
|
317
|
+
const leveloffset = Number(doc.getAttribute('leveloffset') || 0)
|
|
318
|
+
lines[idx] = lines[idx].replace(/^=+( .+)/, (_, rest) => {
|
|
319
|
+
let targetMarkerLength = block.level + (1 - leveloffset) + level + (enclosed ? 1 : 0)
|
|
320
|
+
if (targetMarkerLength > 6) {
|
|
321
|
+
targetMarkerLength = 6
|
|
322
|
+
blockStyle = 'discrete'
|
|
323
|
+
}
|
|
324
|
+
return '='.repeat(targetMarkerLength) + rest
|
|
325
|
+
})
|
|
326
|
+
// NOTE: ID will be undefined if sectids are turned off
|
|
327
|
+
if (block.getId()) rewriteStyleAttribute(block, lines, idx, idprefix, blockStyle)
|
|
328
|
+
} else {
|
|
329
|
+
if (context === 'image') {
|
|
330
|
+
const atImageMacro = (lines[idx] || '').startsWith('image::')
|
|
331
|
+
// NOTE: the following logic is needed only if parser is messing up line number of image block
|
|
332
|
+
//let atImageMacro
|
|
333
|
+
//// NOTE: account for line number tracking error in parser when image has block anchor
|
|
334
|
+
//if (block.getId()) {
|
|
335
|
+
// const originalIdx = idx
|
|
336
|
+
// let line
|
|
337
|
+
// while ((line = lines[idx]) != null && !(atImageMacro = line.startsWith('image::'))) idx++
|
|
338
|
+
// if (!atImageMacro) {
|
|
339
|
+
// idx = originalIdx
|
|
340
|
+
// if ((lines[idx - 1] || '').startsWith('image::')) {
|
|
341
|
+
// atImageMacro = true
|
|
342
|
+
// idx--
|
|
343
|
+
// }
|
|
344
|
+
// }
|
|
345
|
+
//} else if ((lines[idx] || '').startsWith('image::')) {
|
|
346
|
+
// atImageMacro = true
|
|
347
|
+
//}
|
|
348
|
+
const target = block.getAttribute('target')
|
|
349
|
+
if (atImageMacro && isResourceSpec(target)) {
|
|
350
|
+
const image = contentCatalog.resolveResource(target, page.src, 'image', ['image'])
|
|
351
|
+
// FIXME: handle (or report) case when image is not resolved
|
|
352
|
+
if (image) {
|
|
353
|
+
image.out.assembled = true
|
|
354
|
+
const line = lines[idx]
|
|
355
|
+
lines[idx] = `image::${image.pub.url.substr(1)}${line.substr(line.indexOf('['))}`
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
if (block.getId()) rewriteStyleAttribute(block, lines, idx, idprefix)
|
|
360
|
+
}
|
|
361
|
+
})
|
|
362
|
+
buffer.push(...lines)
|
|
363
|
+
const attributeEntries = Object.entries(doc.attributes_defined_in_header || {})
|
|
364
|
+
if (attributeEntries.length) {
|
|
365
|
+
const resolvedAttributeEntries = attributeEntries.reduce(
|
|
366
|
+
(accum, [name, val]) => {
|
|
367
|
+
// Q: couldn't we just check if attribute is locked?
|
|
368
|
+
if (name in mutableAttributes) {
|
|
369
|
+
const initialVal = mutableAttributes[name]
|
|
370
|
+
if (initialVal == null) {
|
|
371
|
+
if (val != null) accum.push(`:!${name}:`)
|
|
372
|
+
} else if (val !== initialVal) {
|
|
373
|
+
accum.push(`:${name}:${initialVal ? ' ' + initialVal : ''}`)
|
|
374
|
+
}
|
|
375
|
+
} else if (val != null && !doc.isAttributeLocked(name)) {
|
|
376
|
+
accum.push(`:!${name}:`)
|
|
377
|
+
}
|
|
378
|
+
return accum
|
|
379
|
+
},
|
|
380
|
+
['']
|
|
381
|
+
)
|
|
382
|
+
if (resolvedAttributeEntries.length > 1) buffer.push(...resolvedAttributeEntries)
|
|
383
|
+
}
|
|
384
|
+
} else {
|
|
385
|
+
if (level) {
|
|
386
|
+
if (level === 1 && navtitle === componentVersion.title) {
|
|
387
|
+
level--
|
|
388
|
+
} else {
|
|
389
|
+
buffer.push('')
|
|
390
|
+
// NOTE: try to toggle sectids; otherwise, fallback to globally unique synthetic ID
|
|
391
|
+
let toggleSectids, syntheticId
|
|
392
|
+
if (!('sectids' in asciidocConfig.attributes)) {
|
|
393
|
+
buffer.push(':!sectids:')
|
|
394
|
+
toggleSectids = true
|
|
395
|
+
} else if (typeof asciidocConfig.attributes.sectids === 'string') {
|
|
396
|
+
if ('sectids' in mutableAttributes) {
|
|
397
|
+
buffer.push(':!sectids:')
|
|
398
|
+
toggleSectids = true
|
|
399
|
+
} else {
|
|
400
|
+
syntheticId = `__object-id-${global.Opal.hash(outlineEntry).$object_id()}`
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
// Q: should we unset docname, page-module, etc?
|
|
404
|
+
const sectionTitle = urlType === 'external' ? `${url}[${navtitle.replace(/\]/, '\\]')}]` : navtitle
|
|
405
|
+
let hlevel = level + 1
|
|
406
|
+
if (hlevel > 6) {
|
|
407
|
+
hlevel = 6
|
|
408
|
+
buffer.push(syntheticId ? `[discrete#${syntheticId}]` : '[discrete]')
|
|
409
|
+
} else if (syntheticId) {
|
|
410
|
+
buffer.push(`[#${syntheticId}]`)
|
|
411
|
+
}
|
|
412
|
+
buffer.push(`${'='.repeat(hlevel)} ${sectionTitle}`)
|
|
413
|
+
if (toggleSectids) buffer.push(':sectids:')
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const nextLevel = level + 1
|
|
419
|
+
if (items) {
|
|
420
|
+
items.forEach((item) => {
|
|
421
|
+
buffer.push(
|
|
422
|
+
...aggregateAsciiDoc(
|
|
423
|
+
loadAsciiDoc,
|
|
424
|
+
contentCatalog,
|
|
425
|
+
header,
|
|
426
|
+
componentVersion,
|
|
427
|
+
item,
|
|
428
|
+
pagesInOutline,
|
|
429
|
+
asciidocConfig,
|
|
430
|
+
mutableAttributes,
|
|
431
|
+
sectionMergeStrategy,
|
|
432
|
+
nextLevel
|
|
433
|
+
)
|
|
434
|
+
)
|
|
435
|
+
})
|
|
436
|
+
}
|
|
437
|
+
return buffer
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function generateStem (componentVersion, title) {
|
|
441
|
+
const { name, version } = componentVersion
|
|
442
|
+
const segments = [name]
|
|
443
|
+
if (version && version !== 'master') segments.push(version)
|
|
444
|
+
segments.push(
|
|
445
|
+
title
|
|
446
|
+
.toLowerCase()
|
|
447
|
+
.replace(/&.+?;|[^ \p{Alpha}0-9_\-.]/gu, '')
|
|
448
|
+
.replace(/[ _.]/g, '-')
|
|
449
|
+
.replace(/--+/g, '-')
|
|
450
|
+
)
|
|
451
|
+
return path.join(...segments)
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function fixSectionLevels (sections, multipart) {
|
|
455
|
+
sections.forEach((sect) => {
|
|
456
|
+
const targetLevel = sect.getParent().getLevel() + 1
|
|
457
|
+
if (multipart ? sect.getLevel() > targetLevel : sect.getLevel() !== targetLevel) sect.level = targetLevel
|
|
458
|
+
if (sect.hasSections()) fixSectionLevels(sect.getSections(), multipart)
|
|
459
|
+
})
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function rewriteStyleAttribute (block, lines, idx, idprefix, replacementStyle = '') {
|
|
463
|
+
let prevLine = lines[idx - 1]
|
|
464
|
+
const char0 = prevLine?.charAt()
|
|
465
|
+
if (char0) {
|
|
466
|
+
if (
|
|
467
|
+
(char0 === '.' && /^\.\.?[^ \t.]/.test(prevLine)) ||
|
|
468
|
+
(char0 === '[' &&
|
|
469
|
+
prevLine.charAt(1) === '[' &&
|
|
470
|
+
/^\[\[(?:|[\p{Alpha}_:][\p{Alpha}0-9_\-:.]*(?:, *.+)?)\]\]$/u.test(prevLine))
|
|
471
|
+
) {
|
|
472
|
+
return rewriteStyleAttribute(block, lines, idx - 1, idprefix, replacementStyle)
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
let cellSpec
|
|
476
|
+
if (
|
|
477
|
+
char0 &&
|
|
478
|
+
(char0 === '[' || (block.getDocument().isNested() && (cellSpec = prevLine.match(/^([^[|]*)\| *(\[.+)/)))) &&
|
|
479
|
+
prevLine.charAt(prevLine.length - 1) === ']'
|
|
480
|
+
) {
|
|
481
|
+
if (cellSpec) {
|
|
482
|
+
prevLine = cellSpec[2]
|
|
483
|
+
cellSpec = cellSpec[1]
|
|
484
|
+
}
|
|
485
|
+
let rawStyle
|
|
486
|
+
const commaIdx = prevLine.indexOf(',')
|
|
487
|
+
if (~commaIdx) {
|
|
488
|
+
rawStyle = prevLine.substr(1, commaIdx - 1)
|
|
489
|
+
if (~rawStyle.indexOf('=')) rawStyle = undefined
|
|
490
|
+
} else if (!~prevLine.indexOf('=')) {
|
|
491
|
+
rawStyle = prevLine.substr(1, prevLine.length - 2)
|
|
492
|
+
}
|
|
493
|
+
if (rawStyle) {
|
|
494
|
+
if (~rawStyle.indexOf('#')) {
|
|
495
|
+
prevLine = prevLine.replace(/#[^.%,\]]+/, `#${idprefix}${block.getId()}`)
|
|
496
|
+
if (replacementStyle) prevLine = prevLine.replace(/^[^#.%,\]]+/, `[${replacementStyle}`)
|
|
497
|
+
} else {
|
|
498
|
+
prevLine = `[${
|
|
499
|
+
replacementStyle ? rawStyle.replace(/^[^.%]*]/, replacementStyle) : rawStyle
|
|
500
|
+
}#${idprefix}${block.getId()}${prevLine.substr(rawStyle.length + 1)}`
|
|
501
|
+
}
|
|
502
|
+
} else {
|
|
503
|
+
prevLine = `[${replacementStyle}#${idprefix}${block.getId()}${rawStyle == null ? ',' : ''}${prevLine.substr(1)}`
|
|
504
|
+
}
|
|
505
|
+
if (cellSpec) prevLine = `${cellSpec}|${prevLine}`
|
|
506
|
+
lines[idx - 1] = prevLine
|
|
507
|
+
} else {
|
|
508
|
+
lines.splice(idx, 0, `[${replacementStyle}#${idprefix}${block.getId()}]`)
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function isResourceSpec (str) {
|
|
513
|
+
return !(~str.indexOf(':') && (~str.indexOf('://') || (str.startsWith('data:') && ~str.indexOf(','))))
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function getObjectId (obj) {
|
|
517
|
+
return global.Opal.uid()
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
module.exports = produceAggregateDocument
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const filterComponentVersions = require('./filter-component-versions')
|
|
4
|
+
const produceAggregateDocument = require('./produce-aggregate-document')
|
|
5
|
+
const selectMutableAttributes = require('./select-mutable-attributes')
|
|
6
|
+
|
|
7
|
+
function produceAggregateDocuments (loadAsciiDoc, contentCatalog, assemblerConfig) {
|
|
8
|
+
const { insertStartPage, rootLevel, sectionMergeStrategy, asciidoc: assemblerAsciiDocConfig } = assemblerConfig
|
|
9
|
+
return filterComponentVersions(contentCatalog.getComponents(), assemblerConfig.componentVersions).reduce(
|
|
10
|
+
(accum, componentVersion) => {
|
|
11
|
+
const { name: componentName, version, title, navigation } = componentVersion
|
|
12
|
+
if (!navigation) return accum
|
|
13
|
+
const componentVersionAsciiDocConfig = getAsciiDocConfigWithAsciidoctorReducerExtension(componentVersion)
|
|
14
|
+
const mergedAsciiDocConfig = Object.assign({}, componentVersionAsciiDocConfig, {
|
|
15
|
+
attributes: Object.assign({}, componentVersionAsciiDocConfig.attributes, assemblerAsciiDocConfig.attributes),
|
|
16
|
+
})
|
|
17
|
+
const rootEntry = { content: title }
|
|
18
|
+
const startPage = contentCatalog.getComponentVersionStartPage(componentName, version)
|
|
19
|
+
let startPageUrl
|
|
20
|
+
if (startPage && insertStartPage) {
|
|
21
|
+
if (includedInNav(navigation, (startPageUrl = startPage.pub.url))) {
|
|
22
|
+
startPageUrl = undefined
|
|
23
|
+
} else {
|
|
24
|
+
Object.assign(rootEntry, { url: startPageUrl, urlType: 'internal' })
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
// Q: should we use an artificial page instead or as fallback?
|
|
28
|
+
const mutableAttributes = startPage
|
|
29
|
+
? selectMutableAttributes(loadAsciiDoc, contentCatalog, startPage, mergedAsciiDocConfig)
|
|
30
|
+
: {}
|
|
31
|
+
return accum.concat(
|
|
32
|
+
prepareOutlines(navigation, rootEntry, rootLevel).map((outline) =>
|
|
33
|
+
produceAggregateDocument(
|
|
34
|
+
loadAsciiDoc,
|
|
35
|
+
contentCatalog,
|
|
36
|
+
componentVersion,
|
|
37
|
+
outline,
|
|
38
|
+
contentCatalog.getPages((page) => page.out),
|
|
39
|
+
mergedAsciiDocConfig,
|
|
40
|
+
mutableAttributes,
|
|
41
|
+
sectionMergeStrategy
|
|
42
|
+
)
|
|
43
|
+
)
|
|
44
|
+
)
|
|
45
|
+
},
|
|
46
|
+
[]
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function getAsciiDocConfigWithAsciidoctorReducerExtension (componentVersion) {
|
|
51
|
+
const asciidoctorReducerExtension = require('./asciidoctor/reducer-extension') // NOTE: must be required lazily
|
|
52
|
+
const asciidocConfig = componentVersion.asciidoc
|
|
53
|
+
const extensions = asciidocConfig.extensions || []
|
|
54
|
+
if (extensions.length) {
|
|
55
|
+
return Object.assign({}, asciidocConfig, {
|
|
56
|
+
extensions: extensions.reduce(
|
|
57
|
+
(accum, candidate) => {
|
|
58
|
+
if (candidate !== asciidoctorReducerExtension) accum.push(candidate)
|
|
59
|
+
return accum
|
|
60
|
+
},
|
|
61
|
+
[asciidoctorReducerExtension]
|
|
62
|
+
),
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
return Object.assign({}, asciidocConfig, { extensions: [asciidoctorReducerExtension], sourcemap: true })
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function includedInNav (items, url) {
|
|
69
|
+
return items.find((it) => it.url === url || includedInNav(it.items || [], url))
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function prepareOutlines (navigation, rootEntry, rootLevel) {
|
|
73
|
+
if (rootLevel === 0 || navigation.length === 1) {
|
|
74
|
+
const navBranch =
|
|
75
|
+
navigation.length === 1
|
|
76
|
+
? navigation[0]
|
|
77
|
+
: { items: navigation.reduce((navTree, it) => navTree.concat(it.content ? it : it.items), []) }
|
|
78
|
+
return [Object.assign(rootEntry, navBranch)]
|
|
79
|
+
}
|
|
80
|
+
return navigation.reduce((navTree, it) => navTree.concat(it.content ? it : it.items), [rootEntry])
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
module.exports = produceAggregateDocuments
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
function selectMutableAttributes (loadAsciiDoc, contentCatalog, referencePage, asciidocConfig) {
|
|
4
|
+
const doc = loadAsciiDoc(
|
|
5
|
+
new referencePage.constructor(
|
|
6
|
+
Object.assign({}, referencePage, { contents: Buffer.alloc(0), mediaType: 'text/asciidoc' })
|
|
7
|
+
),
|
|
8
|
+
contentCatalog,
|
|
9
|
+
asciidocConfig
|
|
10
|
+
)
|
|
11
|
+
const additionalMutableNames = [
|
|
12
|
+
'page-component-name',
|
|
13
|
+
'page-component-version',
|
|
14
|
+
'page-version',
|
|
15
|
+
'page-component-display-version',
|
|
16
|
+
'page-component-title',
|
|
17
|
+
]
|
|
18
|
+
// we could consider using an Asciidoctor extension here to grab attributes passed via the API instead
|
|
19
|
+
const immutableAttributeNames = doc.attribute_overrides.$keys()['$-'](additionalMutableNames)
|
|
20
|
+
return Object.entries(doc.getAttributes()).reduce((accum, [name, val]) => {
|
|
21
|
+
if (!immutableAttributeNames.includes(name)) accum[name] = val
|
|
22
|
+
return accum
|
|
23
|
+
}, {})
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
module.exports = selectMutableAttributes
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { PassThrough } = require('stream')
|
|
4
|
+
|
|
5
|
+
// adapted from https://github.com/jpommerening/node-lazystream/blob/master/lib/lazystream.js | license: MIT
|
|
6
|
+
class LazyReadable extends PassThrough {
|
|
7
|
+
constructor (fn, options) {
|
|
8
|
+
super(options)
|
|
9
|
+
const _read = this._read
|
|
10
|
+
this._read = function () {
|
|
11
|
+
this._read = _read.bind(this)
|
|
12
|
+
fn.call(this, options).on('error', this.emit.bind(this, 'error')).pipe(this)
|
|
13
|
+
return this._read.apply(this, arguments)
|
|
14
|
+
}
|
|
15
|
+
this.emit('readable')
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
module.exports = LazyReadable
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
class PromiseQueue {
|
|
4
|
+
#scheduler
|
|
5
|
+
#started
|
|
6
|
+
#pending
|
|
7
|
+
#rejection
|
|
8
|
+
#startTask
|
|
9
|
+
#trapRejection
|
|
10
|
+
|
|
11
|
+
constructor ({ concurrency = Infinity } = {}) {
|
|
12
|
+
this.concurrency = concurrency
|
|
13
|
+
this.#scheduler = Promise.resolve()
|
|
14
|
+
this.#started = []
|
|
15
|
+
this.#pending = []
|
|
16
|
+
this.#startTask = (task) => {
|
|
17
|
+
if (this.#rejection) return
|
|
18
|
+
if (this.concurrency === Infinity) {
|
|
19
|
+
this.#started.push(task().catch(this.#trapRejection))
|
|
20
|
+
} else {
|
|
21
|
+
let current
|
|
22
|
+
this.#pending.push(
|
|
23
|
+
(current = task()
|
|
24
|
+
.catch(this.#trapRejection)
|
|
25
|
+
.finally(() => this.#pending.splice(this.#pending.indexOf(current), 1)))
|
|
26
|
+
)
|
|
27
|
+
this.#started.push(current)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
this.#trapRejection = (err) => (this.#rejection = err || new Error()) && undefined
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
add (tasks) {
|
|
34
|
+
if (!Array.isArray(tasks)) tasks = [tasks]
|
|
35
|
+
for (const task of tasks) {
|
|
36
|
+
if (this.#pending.length < this.concurrency) {
|
|
37
|
+
this.#startTask(task)
|
|
38
|
+
} else {
|
|
39
|
+
this.#scheduler = this.#scheduler.then(
|
|
40
|
+
() => this.#pending.length && Promise.race(this.#pending).then(() => this.#startTask(task))
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return this
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async toPromise () {
|
|
48
|
+
// what about promiseAll(), all(), or promise()?
|
|
49
|
+
await this.#scheduler
|
|
50
|
+
return Promise.all(this.#started).then((returnValues) => {
|
|
51
|
+
if (this.#rejection) throw this.#rejection
|
|
52
|
+
return returnValues
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
module.exports = PromiseQueue
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const fs = require('fs')
|
|
4
|
+
const LazyReadable = require('./lazy-readable')
|
|
5
|
+
const { spawn } = require('child_process')
|
|
6
|
+
|
|
7
|
+
const IS_WIN = process.platform === 'win32'
|
|
8
|
+
const DBL_QUOTE_RX = /"/g
|
|
9
|
+
|
|
10
|
+
const shellEscape = IS_WIN
|
|
11
|
+
? (val) => {
|
|
12
|
+
if (val.charAt() === '-') {
|
|
13
|
+
return val
|
|
14
|
+
} else if (~val.indexOf('"')) {
|
|
15
|
+
return ~val.indexOf(' ') ? `"${val.replace(DBL_QUOTE_RX, '"""')}"` : val.replace(DBL_QUOTE_RX, '""')
|
|
16
|
+
} else if (~val.indexOf(' ')) {
|
|
17
|
+
return `"${val}"`
|
|
18
|
+
}
|
|
19
|
+
return val
|
|
20
|
+
}
|
|
21
|
+
: (val) => val
|
|
22
|
+
|
|
23
|
+
async function runCommand (cmd, argv = [], opts = {}) {
|
|
24
|
+
if (!cmd) throw new TypeError('Command not specified')
|
|
25
|
+
const cmdv = cmd.split(' ').map(shellEscape)
|
|
26
|
+
const { input, output, implicitStdin, ...spawnOpts } = opts
|
|
27
|
+
if (input) input instanceof Buffer ? implicitStdin || argv.push('-') : argv.push(input)
|
|
28
|
+
if (IS_WIN) Object.assign(spawnOpts, { shell: true, windowsHide: true })
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
30
|
+
const stdout = []
|
|
31
|
+
const stderr = []
|
|
32
|
+
const ps = spawn(cmdv[0], [...cmdv.slice(1), ...argv.map(shellEscape)], spawnOpts)
|
|
33
|
+
ps.on('close', (code) => {
|
|
34
|
+
if (code === 0) {
|
|
35
|
+
if (stderr.length) process.stderr.write(stderr.join(''))
|
|
36
|
+
resolve(output ? new LazyReadable(() => fs.createReadStream(output)) : Buffer.from(stdout.join('')))
|
|
37
|
+
} else {
|
|
38
|
+
let msg = `Command failed: ${ps.spawnargs.join(' ')}`
|
|
39
|
+
if (stderr.length) msg += '\n' + stderr.join('')
|
|
40
|
+
reject(new Error(msg))
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
ps.on('error', (err) => reject(err.code === 'ENOENT' ? new Error(`Command not found: ${cmdv.join(' ')}`) : err))
|
|
44
|
+
ps.stdout.on('data', (data) => (output ? process.stdout.write(data) : stdout.push(data)))
|
|
45
|
+
ps.stderr.on('data', (data) => stderr.push(data))
|
|
46
|
+
try {
|
|
47
|
+
input instanceof Buffer ? ps.stdin.end(input) : ps.stdin.end()
|
|
48
|
+
} catch (err) {
|
|
49
|
+
reject(err)
|
|
50
|
+
} finally {
|
|
51
|
+
ps.stdin.end()
|
|
52
|
+
}
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
module.exports = runCommand
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@antora/assembler",
|
|
3
|
+
"version": "1.0.0-alpha.1",
|
|
4
|
+
"description": "An extension library for Antora that assembles content from multiple pages into a single AsciiDoc file to converted and publish.",
|
|
5
|
+
"license": "MPL-2.0",
|
|
6
|
+
"author": "OpenDevise Inc. (https://opendevise.com)",
|
|
7
|
+
"contributors": [
|
|
8
|
+
"Dan Allen <dan@opendevise.com>",
|
|
9
|
+
"Sarah White <sarah@opendevise.com>"
|
|
10
|
+
],
|
|
11
|
+
"homepage": "https://antora.org",
|
|
12
|
+
"repository": "gitlab:antora/antora-assembler",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://gitlab.com/antora/antora-assembler/issues"
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"test": "_mocha test",
|
|
18
|
+
"prepublishOnly": "node $npm_config_local_prefix/npm/prepublishOnly.js",
|
|
19
|
+
"postpublish": "node $npm_config_local_prefix/npm/postpublish.js"
|
|
20
|
+
},
|
|
21
|
+
"main": "lib/index.js",
|
|
22
|
+
"exports": {
|
|
23
|
+
".": "./lib/index.js",
|
|
24
|
+
"./asciidoctor/reducer-extension": "./lib/asciidoctor/reducer-extension.js",
|
|
25
|
+
"./filter-component-versions": "./lib/filter-component-versions.js",
|
|
26
|
+
"./load-config": "./lib/load-config.js",
|
|
27
|
+
"./produce-aggregate-document": "./lib/produce-aggregate-document.js",
|
|
28
|
+
"./select-mutable-attributes": "./lib/select-mutable-attributes.js"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@antora/expand-path-helper": "~2.0",
|
|
32
|
+
"braces": "~3.0",
|
|
33
|
+
"camelcase-keys": "~7.0",
|
|
34
|
+
"picomatch": "~2.3",
|
|
35
|
+
"vinyl": "~2.2",
|
|
36
|
+
"js-yaml": "~4.1"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@antora/asciidoc-loader": "3.0.1",
|
|
40
|
+
"@antora/site-publisher": "3.0.1"
|
|
41
|
+
},
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=16.0.0"
|
|
44
|
+
},
|
|
45
|
+
"files": [
|
|
46
|
+
"lib/"
|
|
47
|
+
],
|
|
48
|
+
"keywords": [
|
|
49
|
+
"antora",
|
|
50
|
+
"antora-extension",
|
|
51
|
+
"asciidoc",
|
|
52
|
+
"documentation"
|
|
53
|
+
],
|
|
54
|
+
"publishConfig": {
|
|
55
|
+
"access": "public"
|
|
56
|
+
}
|
|
57
|
+
}
|