@antora/assembler 1.0.0-beta.4 → 1.0.0-beta.6
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
CHANGED
|
@@ -51,7 +51,7 @@ async function assembleContent (playbook, contentCatalog, converter, { configSou
|
|
|
51
51
|
const { convert = converter, extname: targetExtname, mediaType: targetMediaType } = converter ?? {}
|
|
52
52
|
if (!(convert && assemblyFiles.length)) return assemblyFiles
|
|
53
53
|
const { publishSite: publishFiles = require('@antora/site-publisher') } = generatorFunctions
|
|
54
|
-
await prepareWorkspace(publishFiles, assemblyFiles,
|
|
54
|
+
await prepareWorkspace(publishFiles, assemblyFiles, buildConfig)
|
|
55
55
|
const boundConvert = convert.bind(context)
|
|
56
56
|
return new PromiseQueue({ concurrency: buildConfig.processLimit })
|
|
57
57
|
.add(
|
|
@@ -100,22 +100,23 @@ async function assembleContent (playbook, contentCatalog, converter, { configSou
|
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
function createResolveAssemblyModel (context, contentCatalog, shared, intrinsicAttributes, navigationCatalog) {
|
|
103
|
+
const logger = context.getLogger ? context.getLogger(PACKAGE_NAME) : undefined
|
|
103
104
|
const { assemblerProfiles } = context.getVariables()
|
|
104
105
|
if (!assemblerProfiles) {
|
|
105
106
|
return (componentVersion) => {
|
|
106
107
|
const navigation =
|
|
107
108
|
navigationCatalog?.getNavigation(componentVersion.name, componentVersion.version) ?? componentVersion.navigation
|
|
108
|
-
return Object.assign({}, shared, { navigation })
|
|
109
|
+
return Object.assign({ logger }, shared, { navigation })
|
|
109
110
|
}
|
|
110
111
|
}
|
|
111
|
-
const boundSendToLog = sendToLog.bind(
|
|
112
|
+
const boundSendToLog = sendToLog.bind(logger)
|
|
112
113
|
const { buildNavigation = require('@antora/navigation-builder') } = context.getFunctions()
|
|
113
114
|
return (componentVersion) => {
|
|
114
115
|
const componentVersionProfiles = assemblerProfiles.get(componentVersion.version + '@' + componentVersion.name)
|
|
115
116
|
const overrides =
|
|
116
117
|
componentVersionProfiles?.get(intrinsicAttributes['assembler-profile']) ?? componentVersionProfiles?.get() ?? {}
|
|
117
118
|
const { navFiles, messages } = overrides
|
|
118
|
-
const model = Object.assign({}, shared, overrides)
|
|
119
|
+
const model = Object.assign({ logger }, shared, overrides)
|
|
119
120
|
delete model.navFiles
|
|
120
121
|
delete model.messages
|
|
121
122
|
const navigationOverride = navigationCatalog?.getNavigation(componentVersion.name, componentVersion.version)
|
|
@@ -177,7 +178,7 @@ function prepareConvertAttributes (doc, targetExtname, buildConfig) {
|
|
|
177
178
|
} else {
|
|
178
179
|
val = `!${name}${val === false ? '@' : ''}`
|
|
179
180
|
}
|
|
180
|
-
args.push(
|
|
181
|
+
args.push(optionFlag, val)
|
|
181
182
|
}
|
|
182
183
|
return args
|
|
183
184
|
},
|
|
@@ -206,7 +207,7 @@ function findFirstExportWithExtname (extname) {
|
|
|
206
207
|
}
|
|
207
208
|
|
|
208
209
|
// TODO: if no workspace dir is defined, we shouldn't continue
|
|
209
|
-
function prepareWorkspace (publishFiles, assemblyFiles,
|
|
210
|
+
function prepareWorkspace (publishFiles, assemblyFiles, buildConfig) {
|
|
210
211
|
const { dir, clean, keepSource } = buildConfig
|
|
211
212
|
const files = []
|
|
212
213
|
const outPaths = new Set()
|
|
@@ -223,7 +224,7 @@ function prepareWorkspace (publishFiles, assemblyFiles, contentCatalog, buildCon
|
|
|
223
224
|
|
|
224
225
|
function isBound (obj) {
|
|
225
226
|
if (obj == null) return false
|
|
226
|
-
for (const
|
|
227
|
+
for (const _ in obj) return true
|
|
227
228
|
return false
|
|
228
229
|
}
|
|
229
230
|
|
package/lib/load-config.js
CHANGED
|
@@ -16,7 +16,11 @@ function loadConfig (playbook, configSource = './antora-assembler.yml') {
|
|
|
16
16
|
() => false
|
|
17
17
|
)
|
|
18
18
|
.then((exists) =>
|
|
19
|
-
exists
|
|
19
|
+
exists
|
|
20
|
+
? fsp
|
|
21
|
+
.readFile(configSource)
|
|
22
|
+
.then((data) => Object.assign(camelCaseKeys(yaml.load(data), ['asciidoc']), { file: configSource }))
|
|
23
|
+
: {}
|
|
20
24
|
)
|
|
21
25
|
).then((config) => {
|
|
22
26
|
if (config.enabled === false) return undefined
|
|
@@ -8,7 +8,7 @@ const unconvertInlineAsciiDoc = require('./util/unconvert-inline-asciidoc')
|
|
|
8
8
|
const AttributeEntryRx = /^:([^:-][^:]*):(?: .*)?$/
|
|
9
9
|
const BuiltInNamedEntities = { amp: '&', apos: "'", gt: '>', lt: '<', nbsp: ' ', quot: '"' }
|
|
10
10
|
const CharRefRx = /&(?:([a-z][a-z]+\d{0,2})|#(?:(\d{2,6})|x([a-z\d]{2,5})));/g
|
|
11
|
-
const DiscardAttributes = 'doctype leveloffset assembly-style underscore'.split(' ')
|
|
11
|
+
const DiscardAttributes = 'doctype leveloffset assembly-navtitle assembly-style underscore'.split(' ')
|
|
12
12
|
const ReservedIdNames = 'content header footnotes footer footer-text premable toc toctitle'.split(' ')
|
|
13
13
|
|
|
14
14
|
function produceAssemblyFile (
|
|
@@ -24,11 +24,10 @@ function produceAssemblyFile (
|
|
|
24
24
|
const pagesByUrl = files.reduce((map, it) => (it.src.family === 'page' ? map.set(it.pub.url, it) : map), new Map())
|
|
25
25
|
if (outline.urlType === 'internal' && !pagesByUrl.get(outline.url) && !(outline.items || []).length) return
|
|
26
26
|
const pagesInOutline = selectPagesInOutline(outline, pagesByUrl, componentVersion)
|
|
27
|
-
const navtitle = outline.content
|
|
28
27
|
const buffer = mergeAsciiDoc(
|
|
29
28
|
loadAsciiDoc,
|
|
30
29
|
contentCatalog,
|
|
31
|
-
buildAsciiDocHeader(componentVersion,
|
|
30
|
+
buildAsciiDocHeader(componentVersion, outline.content, assemblyModel),
|
|
32
31
|
componentVersion,
|
|
33
32
|
outline,
|
|
34
33
|
files,
|
|
@@ -38,7 +37,7 @@ function produceAssemblyFile (
|
|
|
38
37
|
assemblyModel
|
|
39
38
|
)
|
|
40
39
|
const rootLevel = assemblyModel.rootLevel
|
|
41
|
-
const stem = rootLevel === 0 ? 'index' : generateSlug(navtitle)
|
|
40
|
+
const stem = rootLevel === 0 ? 'index' : generateSlug(buffer.navtitle)
|
|
42
41
|
const downloadStem = [componentVersion.name, componentVersion.version, rootLevel === 0 ? '' : stem]
|
|
43
42
|
.filter((it) => it)
|
|
44
43
|
.join('-')
|
|
@@ -63,7 +62,7 @@ function buildAsciiDocHeader (componentVersion, navtitle, assemblyModel) {
|
|
|
63
62
|
let doctitle = navtitleAsciiDoc
|
|
64
63
|
if (navtitlePlain !== componentVersion.title) doctitle = `${componentVersion.title}: ${doctitle}`
|
|
65
64
|
const version = componentVersion.version === 'master' ? '' : componentVersion.version
|
|
66
|
-
|
|
65
|
+
const buffer = [
|
|
67
66
|
`= ${doctitle}`,
|
|
68
67
|
...(version ? [`:revnumber: ${version}`] : []),
|
|
69
68
|
...(doctype === 'article' ? [] : [`:doctype: ${doctype ?? 'book'}`]),
|
|
@@ -75,6 +74,7 @@ function buildAsciiDocHeader (componentVersion, navtitle, assemblyModel) {
|
|
|
75
74
|
`:page-component-display-version: ${componentVersion.displayVersion}`,
|
|
76
75
|
`:page-component-title: ${componentVersion.title}`,
|
|
77
76
|
]
|
|
77
|
+
return Object.assign(buffer, { navtitle })
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
function selectPagesInOutline (outlineEntry, pagesByUrl, componentVersion, accum) {
|
|
@@ -106,13 +106,19 @@ function mergeAsciiDoc (
|
|
|
106
106
|
supportsParts = false
|
|
107
107
|
) {
|
|
108
108
|
// TODO: we could try to be smart about it and make sure the page with fragment is included at least once
|
|
109
|
-
if (outlineEntry.hash)
|
|
110
|
-
|
|
109
|
+
if (outlineEntry.hash) {
|
|
110
|
+
buffer.inBody ??= false
|
|
111
|
+
return buffer
|
|
112
|
+
}
|
|
113
|
+
let navtitle = outlineEntry.content
|
|
114
|
+
let navtitlePlain = sanitize(navtitle)
|
|
115
|
+
let navtitleAsciiDoc = unconvertInlineAsciiDoc(navtitle)
|
|
116
|
+
const { items = [], unresolved, urlType, url } = outlineEntry
|
|
117
|
+
// FIXME: ideally, resource ID would be stored in navigation so we can look up the page more efficiently
|
|
118
|
+
const page = urlType === 'internal' && !unresolved ? pagesInOutline.get(url) : undefined
|
|
111
119
|
const atDocumentRoot = !buffer.inBody
|
|
112
120
|
const atBookRoot = atDocumentRoot && !level && assemblyModel.doctype === 'book' && (supportsParts = true)
|
|
113
121
|
const hasItems = items.length > 0
|
|
114
|
-
const navtitlePlain = sanitize(navtitle)
|
|
115
|
-
const navtitleAsciiDoc = unconvertInlineAsciiDoc(navtitle)
|
|
116
122
|
const siteUrl = ((val) => {
|
|
117
123
|
if (!val || val === '/') return ''
|
|
118
124
|
return val.charAt(val.length - 1) === '/' ? val.slice(0, val.length - 1) : val
|
|
@@ -120,25 +126,23 @@ function mergeAsciiDoc (
|
|
|
120
126
|
const idSeparator = assemblyModel.xmlIds ? '-' : ':'
|
|
121
127
|
const idScopeSeparator = idSeparator.repeat(3)
|
|
122
128
|
const idCoordinateSeparator = idSeparator === '-' ? '----' : idSeparator
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
// IMPORTANT: this must not leave behind lines the parser will drop!
|
|
131
|
-
// IDEA: another option is to capture initial lineno of reader and use as offset (but preseves those blank lines)
|
|
132
|
-
contents = Buffer.from(
|
|
133
|
-
contents
|
|
134
|
-
.toString()
|
|
135
|
-
.replace(/^(?:[ \t]*\r\n?|[ \t]*\n)+/, '')
|
|
136
|
-
.trimRight()
|
|
137
|
-
)
|
|
138
|
-
const { component, version, module: module_, relative, origin } = page.src
|
|
129
|
+
if (page && !pagesInOutline.assembled.pages.has(page)) {
|
|
130
|
+
const contents = page.src.contents
|
|
131
|
+
if (contents == null) {
|
|
132
|
+
buffer.inBody ??= false
|
|
133
|
+
return buffer
|
|
134
|
+
}
|
|
135
|
+
const { component, version, module: module_, relative, origin, mediaType } = page.src
|
|
139
136
|
const topicPrefix = ~relative.indexOf('/') ? path.dirname(relative) + '/' : ''
|
|
140
|
-
const pageAsAsciiDoc = new page.constructor(
|
|
137
|
+
const pageAsAsciiDoc = new page.constructor(
|
|
138
|
+
Object.assign({}, page, { contents: trimAsciiDoc(contents), mediaType })
|
|
139
|
+
)
|
|
141
140
|
const doc = loadAsciiDoc(pageAsAsciiDoc, contentCatalog, asciidocConfig)
|
|
141
|
+
if (doc.hasAttribute('assembly-navtitle')) {
|
|
142
|
+
navtitleAsciiDoc = doc.getAttribute('assembly-navtitle')
|
|
143
|
+
navtitlePlain = sanitize((navtitle = doc.$apply_reftext_subs(navtitleAsciiDoc)))
|
|
144
|
+
if (buffer.inBody == null) buffer.navtitle = navtitle
|
|
145
|
+
}
|
|
142
146
|
if (atDocumentRoot) {
|
|
143
147
|
const authors = doc.getAuthors()
|
|
144
148
|
if (authors.length) {
|
|
@@ -198,7 +202,6 @@ function mergeAsciiDoc (
|
|
|
198
202
|
}
|
|
199
203
|
buffer.push(`:page-module: ${module_}`)
|
|
200
204
|
buffer.push(`:page-relative-src-path: ${relative}`)
|
|
201
|
-
//buffer.push(`:page-origin-type: ${origin.type}`)
|
|
202
205
|
buffer.push(`:page-origin-url: ${origin.url}`)
|
|
203
206
|
buffer.push(`:page-origin-start-path:${origin.startPath && ' '}${origin.startPath}`)
|
|
204
207
|
buffer.push(`:page-origin-refname: ${origin.branch || origin.tag}`)
|
|
@@ -400,9 +403,9 @@ function mergeAsciiDoc (
|
|
|
400
403
|
if (~hashIdx && !relativePart.endsWith('.adoc')) relativePart += '.adoc'
|
|
401
404
|
}
|
|
402
405
|
if (!isPage || ~relativePart.indexOf('@') || /:.*:/.test(relativePart)) {
|
|
403
|
-
if (siteUrl && (targetResource = contentCatalog.resolveResource(relativePart, page.src, 'page'))?.
|
|
406
|
+
if (siteUrl && (targetResource = contentCatalog.resolveResource(relativePart, page.src, 'page'))?.pub) {
|
|
404
407
|
text ||= targetResource.asciidoc?.xreftext || target
|
|
405
|
-
return `${siteUrl}${targetResource.pub.url}${fragment && '#' + fragment}[${text}]`
|
|
408
|
+
return `${siteUrl}${targetResource.pub.url.replace(/_/g, '{underscore}')}${fragment && '#' + fragment}[${text}]`
|
|
406
409
|
}
|
|
407
410
|
// TODO: handle unresolved resource better
|
|
408
411
|
return m
|
|
@@ -555,6 +558,7 @@ function mergeAsciiDoc (
|
|
|
555
558
|
}
|
|
556
559
|
} else if (level) {
|
|
557
560
|
if (atDocumentRoot && navtitlePlain === componentVersion.title) {
|
|
561
|
+
buffer.inBody ??= false
|
|
558
562
|
level--
|
|
559
563
|
} else {
|
|
560
564
|
buffer.inBody = true
|
|
@@ -663,6 +667,12 @@ function processDocumentHeader (doc, lines, buffer, ignoreLines) {
|
|
|
663
667
|
} else if (belowDoctitle) {
|
|
664
668
|
if (implicitLines.length === 2 || !/[\p{Alpha}0-9]/u.test(chr0)) break
|
|
665
669
|
implicitLines.push(line)
|
|
670
|
+
} else if (chr0 === '[' && line.charAt(line.length - 1) === ']') {
|
|
671
|
+
const attrlist = line
|
|
672
|
+
.slice(1, -1)
|
|
673
|
+
.trim()
|
|
674
|
+
.replace(/(?:^\w[\w-]*(?!=|[\w-]))?([#.%]\w[\w-]*)*/, '')
|
|
675
|
+
if (attrlist) buffer.push('[' + attrlist + ']')
|
|
666
676
|
}
|
|
667
677
|
} else if (belowDoctitle) {
|
|
668
678
|
break
|
|
@@ -685,7 +695,7 @@ function generateSlug (title) {
|
|
|
685
695
|
return String.fromCharCode(dec ? parseInt(dec, 10) : parseInt(hex, 16))
|
|
686
696
|
})
|
|
687
697
|
.replace(/[\x27\u2019]/g, '')
|
|
688
|
-
.replace(/[^\p{Alpha}0-9
|
|
698
|
+
.replace(/[^\p{Alpha}0-9-]/gu, '-')
|
|
689
699
|
.replace(/^-+|-+$|(-)-+/g, '$1')
|
|
690
700
|
}
|
|
691
701
|
|
|
@@ -752,7 +762,19 @@ function isResourceSpec (str) {
|
|
|
752
762
|
}
|
|
753
763
|
|
|
754
764
|
function getObjectId (obj) {
|
|
755
|
-
return global.Opal.
|
|
765
|
+
return global.Opal.id(obj)
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// NOTE: blank lines at top and bottom of document create mismatch when using line numbers to navigate source lines
|
|
769
|
+
// IMPORTANT: this must not leave behind lines the parser will drop!
|
|
770
|
+
// IDEA: another option is to capture initial lineno of reader and use as offset (but preseves those blank lines)
|
|
771
|
+
function trimAsciiDoc (buffer) {
|
|
772
|
+
return Buffer.from(
|
|
773
|
+
buffer
|
|
774
|
+
.toString()
|
|
775
|
+
.replace(/^(?:[ \t]*\r\n?|[ \t]*\n)+/, '')
|
|
776
|
+
.trimRight()
|
|
777
|
+
)
|
|
756
778
|
}
|
|
757
779
|
|
|
758
780
|
function safePush (onto, entries) {
|
|
@@ -5,6 +5,7 @@ const filterComponentVersions = require('./filter-component-versions')
|
|
|
5
5
|
const produceAssemblyFile = require('./produce-assembly-file')
|
|
6
6
|
const selectMutableAttributes = require('./select-mutable-attributes')
|
|
7
7
|
|
|
8
|
+
const ATTR_REF_RX = /\\?\{(\w[\w-]*)\}/g
|
|
8
9
|
const IMAGE_MACRO_RX = /^image::?(.+?)\[(.*?)\]$/
|
|
9
10
|
|
|
10
11
|
function produceAssemblyFiles (loadAsciiDoc, contentCatalog, assemblerConfig, resolveAssemblyModel) {
|
|
@@ -22,16 +23,21 @@ function produceAssemblyFiles (loadAsciiDoc, contentCatalog, assemblerConfig, re
|
|
|
22
23
|
delete assemblerAsciiDocAttributes.revdate
|
|
23
24
|
delete assemblerAsciiDocAttributes['source-highlighter']
|
|
24
25
|
const publishableFiles = contentCatalog.getFiles().filter((file) => file.out)
|
|
26
|
+
const configMdc = assemblerConfig.file ? { file: { path: assemblerConfig.file } } : {}
|
|
25
27
|
return filterComponentVersions(contentCatalog.getComponents(), assemblerConfig.componentVersionFilter.names).reduce(
|
|
26
28
|
(accum, componentVersion) => {
|
|
27
29
|
const assemblyModel = resolveAssemblyModel(componentVersion)
|
|
28
30
|
if (!assemblyModel.navigation) return accum
|
|
29
31
|
const { name: componentName, version, title } = componentVersion
|
|
30
32
|
const componentVersionAsciiDocConfig = getAsciiDocConfigWithAsciidoctorReducerExtension(componentVersion)
|
|
33
|
+
const mergedAsciiDocAttributes = collateAsciiDocAttributes(
|
|
34
|
+
Object.assign({ revdate }, componentVersionAsciiDocConfig.attributes),
|
|
35
|
+
assemblerAsciiDocAttributes,
|
|
36
|
+
{ logger: assemblyModel.logger, mdc: configMdc }
|
|
37
|
+
)
|
|
31
38
|
const mergedAsciiDocConfig = Object.assign({}, componentVersionAsciiDocConfig, {
|
|
32
|
-
attributes:
|
|
39
|
+
attributes: mergedAsciiDocAttributes,
|
|
33
40
|
})
|
|
34
|
-
const mergedAsciiDocAttributes = mergedAsciiDocConfig.attributes
|
|
35
41
|
const auxiliaryImages = new Set()
|
|
36
42
|
Object.entries(mergedAsciiDocAttributes).forEach(([name, val]) => {
|
|
37
43
|
const match = name.endsWith('-image') && val.startsWith('image:') && IMAGE_MACRO_RX.exec(val)
|
|
@@ -142,4 +148,33 @@ function prepareOutlines (navigation, rootEntry, rootLevel) {
|
|
|
142
148
|
return items
|
|
143
149
|
}
|
|
144
150
|
|
|
151
|
+
function collateAsciiDocAttributes (collated, additional, { logger, mdc }) {
|
|
152
|
+
Object.entries(additional).forEach(([name, val]) => {
|
|
153
|
+
if (val && val.constructor === String) {
|
|
154
|
+
let alias
|
|
155
|
+
val = val.replace(ATTR_REF_RX, (ref, refname) => {
|
|
156
|
+
if (ref.charAt() === '\\') return ref.substr(1)
|
|
157
|
+
const refval = collated[refname]
|
|
158
|
+
if (refval == null || refval === false) {
|
|
159
|
+
if (refname in collated && ref === val) {
|
|
160
|
+
alias = refval
|
|
161
|
+
} else if (collated['attribute-missing'] === 'warn') {
|
|
162
|
+
if (logger) {
|
|
163
|
+
logger.warn(mdc, "Skipping reference to missing attribute '%s' in value of '%s' attribute", refname, name)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return ref
|
|
167
|
+
}
|
|
168
|
+
if (refval.constructor === String) return refval
|
|
169
|
+
if (ref !== val) return refval.toString()
|
|
170
|
+
alias = refval
|
|
171
|
+
return ref
|
|
172
|
+
})
|
|
173
|
+
if (alias !== undefined) val = alias
|
|
174
|
+
}
|
|
175
|
+
collated[name] = val
|
|
176
|
+
})
|
|
177
|
+
return collated
|
|
178
|
+
}
|
|
179
|
+
|
|
145
180
|
module.exports = produceAssemblyFiles
|
package/lib/util/compute-out.js
CHANGED
|
@@ -5,7 +5,7 @@ const { posix: path } = require('node:path')
|
|
|
5
5
|
function computeOut (src) {
|
|
6
6
|
const { component, version, module: module_, family, relative } = src
|
|
7
7
|
const outRelative = family === 'page' ? relative.replace(/\.adoc$/, '.html') : relative
|
|
8
|
-
const { dir: dirname, base: basename
|
|
8
|
+
const { dir: dirname, base: basename } = path.parse(outRelative)
|
|
9
9
|
const componentVersion = this.getComponentVersion(component, version)
|
|
10
10
|
const versionSegment =
|
|
11
11
|
'activeVersionSegment' in componentVersion
|
|
@@ -48,7 +48,7 @@ function resolveActiveVersionSegment (component, version) {
|
|
|
48
48
|
this.removeFile((startPage = this.addFile({ src: startPageSrc })))
|
|
49
49
|
}
|
|
50
50
|
const outPathSegments = startPage.out.path.split('/')
|
|
51
|
-
for (const
|
|
51
|
+
for (const _ of startPage.out.moduleRootPath.split('/')) outPathSegments.pop()
|
|
52
52
|
if (startPageSrc.module !== 'ROOT') outPathSegments.pop()
|
|
53
53
|
if (startPageSrc.component !== 'ROOT') outPathSegments.shift()
|
|
54
54
|
return outPathSegments.length ? outPathSegments[0] : ''
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@antora/assembler",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.6",
|
|
4
4
|
"description": "A JavaScript library that merges AsciiDoc content from multiple pages in an Antora site into assembly files and delegates to an exporter to convert those files to another format, such as PDF.",
|
|
5
5
|
"license": "MPL-2.0",
|
|
6
6
|
"author": "OpenDevise Inc. (https://opendevise.com)",
|