@antora/assembler 1.0.0-beta.1 → 1.0.0-beta.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/adapters/asciidoctor/jsonl-logger.rb +25 -0
- package/lib/assemble-content.js +94 -25
- package/lib/configure.js +5 -8
- package/lib/load-config.js +30 -3
- package/lib/produce-assembly-file.js +183 -104
- package/lib/produce-assembly-files.js +72 -3
- package/lib/util/compute-out.js +3 -3
- package/lib/util/create-asciidoc-file.js +0 -1
- package/lib/util/parse-resource-ref.js +36 -0
- package/package.json +4 -1
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
autoload :JSON, 'json'
|
|
4
|
+
|
|
5
|
+
class JsonlFormatter < Asciidoctor::Logger::BasicFormatter
|
|
6
|
+
def call severity, time, progname, msg
|
|
7
|
+
entry = { 'time' => time.utc.to_i, 'level' => severity.downcase, 'name' => progname }
|
|
8
|
+
if ::String === msg
|
|
9
|
+
entry['msg'] = msg
|
|
10
|
+
else
|
|
11
|
+
loc = msg[:source_location]
|
|
12
|
+
entry['file'] = { 'path' => loc.file, 'line' => loc.lineno }
|
|
13
|
+
entry['msg'] = msg[:text]
|
|
14
|
+
end
|
|
15
|
+
(JSON.generate entry) + ?\n
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
proc do
|
|
20
|
+
logger = Asciidoctor::LoggerManager.logger
|
|
21
|
+
logger.formatter = JsonlFormatter.new
|
|
22
|
+
unless (progname = ::File.basename $PROGRAM_NAME).empty?
|
|
23
|
+
logger.progname = progname
|
|
24
|
+
end
|
|
25
|
+
end.call
|
package/lib/assemble-content.js
CHANGED
|
@@ -13,6 +13,7 @@ const { stringify: toJSON } = JSON
|
|
|
13
13
|
|
|
14
14
|
const invariably = { new: () => ({}), void: () => undefined }
|
|
15
15
|
const PACKAGE_NAME = require('../package.json').name
|
|
16
|
+
const NEWLINE_RX = /(?:\r?\n)+/
|
|
16
17
|
|
|
17
18
|
async function assembleContent (playbook, contentCatalog, converter, { configSource, navigationCatalog }) {
|
|
18
19
|
const assemblerConfig = await loadConfig(playbook, configSource)
|
|
@@ -26,10 +27,20 @@ async function assembleContent (playbook, contentCatalog, converter, { configSou
|
|
|
26
27
|
}
|
|
27
28
|
const generatorFunctions = context.getFunctions()
|
|
28
29
|
const { loadAsciiDoc = require('@antora/asciidoc-loader') } = generatorFunctions
|
|
29
|
-
const
|
|
30
|
+
const {
|
|
31
|
+
convert = converter,
|
|
32
|
+
getDefaultCommand,
|
|
33
|
+
extname: targetExtname = '',
|
|
34
|
+
backend: targetBackend = targetExtname.slice(1),
|
|
35
|
+
embedReferenceStyle = 'relative',
|
|
36
|
+
mediaType: targetMediaType,
|
|
37
|
+
loggerName = PACKAGE_NAME,
|
|
38
|
+
} = converter ?? {}
|
|
30
39
|
const { assembly: assemblyConfig, build: buildConfig } = assemblerConfig
|
|
40
|
+
assemblyConfig.embedReferenceStyle = embedReferenceStyle
|
|
31
41
|
const { profile = targetBackend } = assemblyConfig
|
|
32
42
|
const intrinsicAttributes = { 'loader-assembler': '' }
|
|
43
|
+
buildConfig.cwd ??= process.cwd()
|
|
33
44
|
if (profile) {
|
|
34
45
|
buildConfig.dir ??= ospath.join(playbook.dir ?? process.cwd(), `build/assembler-${profile}`)
|
|
35
46
|
intrinsicAttributes[`assembler-profile-${profile}`] = ''
|
|
@@ -41,6 +52,11 @@ async function assembleContent (playbook, contentCatalog, converter, { configSou
|
|
|
41
52
|
} else {
|
|
42
53
|
buildConfig.dir ??= ospath.join(playbook.dir ?? process.cwd(), 'build/assembler')
|
|
43
54
|
}
|
|
55
|
+
if (targetExtname) {
|
|
56
|
+
const targetFiletype = targetExtname.slice(1)
|
|
57
|
+
intrinsicAttributes[`assembler-filetype-${targetFiletype}`] = ''
|
|
58
|
+
intrinsicAttributes['assembler-filetype'] = targetFiletype
|
|
59
|
+
}
|
|
44
60
|
Object.assign(assemblerConfig.asciidoc.attributes, intrinsicAttributes)
|
|
45
61
|
const assemblyFiles = produceAssemblyFiles(
|
|
46
62
|
loadAsciiDoc,
|
|
@@ -48,21 +64,23 @@ async function assembleContent (playbook, contentCatalog, converter, { configSou
|
|
|
48
64
|
assemblerConfig,
|
|
49
65
|
createResolveAssemblyModel(context, contentCatalog, assemblyConfig, intrinsicAttributes, navigationCatalog)
|
|
50
66
|
)
|
|
51
|
-
|
|
52
|
-
if (
|
|
67
|
+
if (!(assemblyFiles.length && typeof convert === 'function')) return assemblyFiles
|
|
68
|
+
if (buildConfig.command == null && typeof getDefaultCommand === 'function') {
|
|
69
|
+
buildConfig.command = await getDefaultCommand(buildConfig.cwd)
|
|
70
|
+
}
|
|
53
71
|
const { publishSite: publishFiles = require('@antora/site-publisher') } = generatorFunctions
|
|
54
|
-
await prepareWorkspace(publishFiles, assemblyFiles,
|
|
72
|
+
await prepareWorkspace(publishFiles, assemblyFiles, buildConfig)
|
|
55
73
|
const boundConvert = convert.bind(context)
|
|
56
74
|
return new PromiseQueue({ concurrency: buildConfig.processLimit })
|
|
57
75
|
.add(
|
|
58
76
|
assemblyFiles.map((doc) => async () => {
|
|
59
|
-
const
|
|
77
|
+
const relativeToOutput = embedReferenceStyle === 'output-relative'
|
|
78
|
+
const convertAttributes = prepareConvertAttributes(doc, targetExtname, relativeToOutput, buildConfig)
|
|
60
79
|
if (buildConfig.mkdirs) await fsp.mkdir(convertAttributes.outdir, { recursive: true, force: true })
|
|
61
|
-
return boundConvert(doc, convertAttributes, buildConfig).then(
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
)
|
|
80
|
+
return boundConvert(doc, convertAttributes, buildConfig).then((result) => {
|
|
81
|
+
const fileOrContents = resolveFileOrContents.call(context, result, convertAttributes, buildConfig, loggerName)
|
|
82
|
+
return coerceToExportFormat(doc, targetBackend, targetExtname, targetMediaType, fileOrContents)
|
|
83
|
+
})
|
|
66
84
|
})
|
|
67
85
|
)
|
|
68
86
|
.toPromise()
|
|
@@ -85,7 +103,13 @@ async function assembleContent (playbook, contentCatalog, converter, { configSou
|
|
|
85
103
|
}
|
|
86
104
|
pages.forEach((fragment, page) => {
|
|
87
105
|
const assemblerMeta = (page.assembler ??= {})
|
|
88
|
-
|
|
106
|
+
const exports = (assemblerMeta.exports ??= [])
|
|
107
|
+
const exportEntry = { fragment, file }
|
|
108
|
+
const insertIdx = exports.findIndex(
|
|
109
|
+
({ file: candidate }) =>
|
|
110
|
+
!(candidate.src.component === page.src.component && candidate.src.version === page.src.version)
|
|
111
|
+
)
|
|
112
|
+
~insertIdx ? exports.splice(insertIdx, 0, exportEntry) : exports.push(exportEntry)
|
|
89
113
|
const extnameProp = extname.slice(1)
|
|
90
114
|
if (extnameProp in assemblerMeta) return
|
|
91
115
|
Object.defineProperty(assemblerMeta, extnameProp, {
|
|
@@ -99,24 +123,26 @@ async function assembleContent (playbook, contentCatalog, converter, { configSou
|
|
|
99
123
|
})
|
|
100
124
|
}
|
|
101
125
|
|
|
102
|
-
function createResolveAssemblyModel (context, contentCatalog,
|
|
126
|
+
function createResolveAssemblyModel (context, contentCatalog, common, intrinsicAttributes, navigationCatalog) {
|
|
127
|
+
const logger = context.getLogger?.(PACKAGE_NAME)
|
|
103
128
|
const { assemblerProfiles } = context.getVariables()
|
|
104
|
-
const { insertStartPage, rootLevel, sectionMergeStrategy } = defaults
|
|
105
129
|
if (!assemblerProfiles) {
|
|
106
130
|
return (componentVersion) => {
|
|
107
131
|
const navigation =
|
|
108
132
|
navigationCatalog?.getNavigation(componentVersion.name, componentVersion.version) ?? componentVersion.navigation
|
|
109
|
-
return {
|
|
133
|
+
return Object.assign({ logger }, common, { navigation })
|
|
110
134
|
}
|
|
111
135
|
}
|
|
112
|
-
const boundSendToLog = sendToLog.bind(
|
|
136
|
+
const boundSendToLog = sendToLog.bind(logger)
|
|
113
137
|
const { buildNavigation = require('@antora/navigation-builder') } = context.getFunctions()
|
|
114
138
|
return (componentVersion) => {
|
|
115
139
|
const componentVersionProfiles = assemblerProfiles.get(componentVersion.version + '@' + componentVersion.name)
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
)
|
|
140
|
+
const overrides =
|
|
141
|
+
componentVersionProfiles?.get(intrinsicAttributes['assembler-profile']) ?? componentVersionProfiles?.get() ?? {}
|
|
142
|
+
const { navFiles, messages } = overrides
|
|
143
|
+
const model = Object.assign({ logger }, common, overrides)
|
|
144
|
+
delete model.navFiles
|
|
145
|
+
delete model.messages
|
|
120
146
|
const navigationOverride = navigationCatalog?.getNavigation(componentVersion.name, componentVersion.version)
|
|
121
147
|
if (navigationOverride) return Object.assign(model, { navigation: navigationOverride })
|
|
122
148
|
messages?.forEach(boundSendToLog)
|
|
@@ -143,7 +169,7 @@ function createResolveAssemblyModel (context, contentCatalog, defaults, intrinsi
|
|
|
143
169
|
}
|
|
144
170
|
}
|
|
145
171
|
|
|
146
|
-
function prepareConvertAttributes (doc, targetExtname, buildConfig) {
|
|
172
|
+
function prepareConvertAttributes (doc, targetExtname, relativeToOutput, buildConfig) {
|
|
147
173
|
const {
|
|
148
174
|
asciidoc: { attributes: docAttributes } = { attributes: {} },
|
|
149
175
|
extname: docfilesuffix,
|
|
@@ -153,7 +179,8 @@ function prepareConvertAttributes (doc, targetExtname, buildConfig) {
|
|
|
153
179
|
const { cwd = process.cwd(), dir = cwd } = buildConfig
|
|
154
180
|
const docname = family + '$' + relative.slice(0, relative.length - docfilesuffix.length)
|
|
155
181
|
const docfile = ospath.join(dir, reldocfile)
|
|
156
|
-
const
|
|
182
|
+
const outdir = ospath.dirname(docfile)
|
|
183
|
+
const docdir = relativeToOutput ? dir : outdir
|
|
157
184
|
const imagesdir = ''
|
|
158
185
|
const outfile = docfile.slice(0, docfile.length - docfilesuffix.length) + targetExtname
|
|
159
186
|
const attributes = Object.assign({}, docAttributes, {
|
|
@@ -162,7 +189,7 @@ function prepareConvertAttributes (doc, targetExtname, buildConfig) {
|
|
|
162
189
|
docfilesuffix,
|
|
163
190
|
'docname@': docname,
|
|
164
191
|
imagesdir,
|
|
165
|
-
outdir
|
|
192
|
+
outdir,
|
|
166
193
|
outfile,
|
|
167
194
|
outfilesuffix: targetExtname,
|
|
168
195
|
toArgs (optionFlag, command) {
|
|
@@ -172,11 +199,15 @@ function prepareConvertAttributes (doc, targetExtname, buildConfig) {
|
|
|
172
199
|
if (val) {
|
|
173
200
|
val = `${name}=${padCharRef && typeof val.charAt === 'function' && val.charAt() === '&' ? ' ' : ''}${val}`
|
|
174
201
|
} else if (val === '') {
|
|
202
|
+
if (name === 'asciidoctor-log-integration') {
|
|
203
|
+
args.push('-r', require.resolve('#asciidoctor-log-adapter'))
|
|
204
|
+
continue
|
|
205
|
+
}
|
|
175
206
|
val = name
|
|
176
207
|
} else {
|
|
177
208
|
val = `!${name}${val === false ? '@' : ''}`
|
|
178
209
|
}
|
|
179
|
-
args.push(
|
|
210
|
+
args.push(optionFlag, val)
|
|
180
211
|
}
|
|
181
212
|
return args
|
|
182
213
|
},
|
|
@@ -205,7 +236,7 @@ function findFirstExportWithExtname (extname) {
|
|
|
205
236
|
}
|
|
206
237
|
|
|
207
238
|
// TODO: if no workspace dir is defined, we shouldn't continue
|
|
208
|
-
function prepareWorkspace (publishFiles, assemblyFiles,
|
|
239
|
+
function prepareWorkspace (publishFiles, assemblyFiles, buildConfig) {
|
|
209
240
|
const { dir, clean, keepSource } = buildConfig
|
|
210
241
|
const files = []
|
|
211
242
|
const outPaths = new Set()
|
|
@@ -220,9 +251,47 @@ function prepareWorkspace (publishFiles, assemblyFiles, contentCatalog, buildCon
|
|
|
220
251
|
return publishFiles({ output: { clean, dir } }, { getFiles: () => files })
|
|
221
252
|
}
|
|
222
253
|
|
|
254
|
+
function resolveFileOrContents (convertResult, convertAttributes, buildConfig, loggerName) {
|
|
255
|
+
let fileOrContents
|
|
256
|
+
if (convertResult?.status != null) {
|
|
257
|
+
fileOrContents = 'file' in convertResult ? convertResult.file : convertResult.contents
|
|
258
|
+
let logger, match
|
|
259
|
+
if (buildConfig.stderrSink === 'log' && convertResult.stderr.length && (logger = this.getLogger(loggerName))) {
|
|
260
|
+
const docfile = convertAttributes.docfile
|
|
261
|
+
const command = buildConfig.command
|
|
262
|
+
const stderr = convertResult.stderr.toString().trimEnd()
|
|
263
|
+
stderr.split(NEWLINE_RX).forEach((line) => {
|
|
264
|
+
const ctx = { command, file: { path: docfile } }
|
|
265
|
+
if (line.charAt() === '{' && line.charAt(line.length - 1) === '}') {
|
|
266
|
+
const entry = JSON.parse(line)
|
|
267
|
+
if (entry.name) ctx.program = entry.name
|
|
268
|
+
if (entry.file?.line) ctx.line = entry.file.line
|
|
269
|
+
logger[entry.level](ctx, entry.msg)
|
|
270
|
+
} else if ((match = /^([^:]+):(\d+): warning: (.+)/.exec(line))) {
|
|
271
|
+
const [, scriptPath, lineno, msg] = match
|
|
272
|
+
ctx.stack = [{ file: { path: scriptPath }, line: parseInt(lineno, 10) }]
|
|
273
|
+
logger.warn(ctx, msg)
|
|
274
|
+
} else if ((match = /^asciidoctor: ([A-Z]+): (?:[^:]+: line (\d+): )?(.+)/.exec(line))) {
|
|
275
|
+
// NOTE we don't care about the filename in the message since it can only be docfile
|
|
276
|
+
const [, level, lineno, msg] = match
|
|
277
|
+
if (lineno) ctx.line = parseInt(lineno, 10)
|
|
278
|
+
logger[level === 'WARNING' ? 'warn' : level.toLowerCase()](ctx, msg)
|
|
279
|
+
} else {
|
|
280
|
+
logger.info(ctx, line)
|
|
281
|
+
}
|
|
282
|
+
})
|
|
283
|
+
}
|
|
284
|
+
} else if (convertResult !== undefined) {
|
|
285
|
+
return convertResult
|
|
286
|
+
}
|
|
287
|
+
return fileOrContents === undefined
|
|
288
|
+
? new LazyReadable(() => fs.createReadStream(convertAttributes.outfile))
|
|
289
|
+
: fileOrContents
|
|
290
|
+
}
|
|
291
|
+
|
|
223
292
|
function isBound (obj) {
|
|
224
293
|
if (obj == null) return false
|
|
225
|
-
for (const
|
|
294
|
+
for (const _ in obj) return true
|
|
226
295
|
return false
|
|
227
296
|
}
|
|
228
297
|
|
package/lib/configure.js
CHANGED
|
@@ -6,7 +6,8 @@ function configure (context, ...args) {
|
|
|
6
6
|
|
|
7
7
|
function internalConfigure (converter, config = {}, providers = {}) {
|
|
8
8
|
this.once('componentsRegistered', ({ contentCatalog, assemblerProfiles }) => {
|
|
9
|
-
|
|
9
|
+
if (assemblerProfiles) return
|
|
10
|
+
this.updateVariables({ assemblerProfiles: getAssemblerProfiles(contentCatalog) })
|
|
10
11
|
})
|
|
11
12
|
|
|
12
13
|
this.once('beforeProcess', ({ siteAsciiDocConfig }) => {
|
|
@@ -64,14 +65,10 @@ function getAssemblerProfiles (contentCatalog, assemblerProfiles = new Map()) {
|
|
|
64
65
|
}
|
|
65
66
|
|
|
66
67
|
function getAssemblerConfigFromDescriptor (descriptor = {}) {
|
|
67
|
-
|
|
68
|
+
const assemblerConfig = descriptor.ext?.assembler
|
|
68
69
|
if (!assemblerConfig) return
|
|
69
|
-
if (Array.isArray(assemblerConfig))
|
|
70
|
-
|
|
71
|
-
} else {
|
|
72
|
-
assemblerConfig = [assemblerConfig]
|
|
73
|
-
}
|
|
74
|
-
return assemblerConfig
|
|
70
|
+
if (!Array.isArray(assemblerConfig)) return [assemblerConfig]
|
|
71
|
+
if (assemblerConfig.length) return assemblerConfig
|
|
75
72
|
}
|
|
76
73
|
|
|
77
74
|
function initNavFile (file, component, version, index) {
|
package/lib/load-config.js
CHANGED
|
@@ -5,6 +5,14 @@ const fsp = require('node:fs/promises')
|
|
|
5
5
|
const os = require('node:os')
|
|
6
6
|
const yaml = require('js-yaml')
|
|
7
7
|
|
|
8
|
+
const ASSEMBLY_KEYS = [
|
|
9
|
+
'rootLevel',
|
|
10
|
+
'insertStartPage',
|
|
11
|
+
'sectionMergeStrategy',
|
|
12
|
+
'linkReferenceStyle',
|
|
13
|
+
'dropExplicitXrefText',
|
|
14
|
+
]
|
|
15
|
+
|
|
8
16
|
function loadConfig (playbook, configSource = './antora-assembler.yml') {
|
|
9
17
|
return (
|
|
10
18
|
configSource.constructor === Object
|
|
@@ -16,7 +24,11 @@ function loadConfig (playbook, configSource = './antora-assembler.yml') {
|
|
|
16
24
|
() => false
|
|
17
25
|
)
|
|
18
26
|
.then((exists) =>
|
|
19
|
-
exists
|
|
27
|
+
exists
|
|
28
|
+
? fsp
|
|
29
|
+
.readFile(configSource)
|
|
30
|
+
.then((data) => Object.assign(camelCaseKeys(yaml.load(data), ['asciidoc']), { file: configSource }))
|
|
31
|
+
: {}
|
|
20
32
|
)
|
|
21
33
|
).then((config) => {
|
|
22
34
|
if (config.enabled === false) return undefined
|
|
@@ -42,7 +54,7 @@ function loadConfig (playbook, configSource = './antora-assembler.yml') {
|
|
|
42
54
|
const remapAssemblyKeys = !('assembly' in config)
|
|
43
55
|
const assembly = (config.assembly ??= {})
|
|
44
56
|
if (remapAssemblyKeys) {
|
|
45
|
-
for (const key of
|
|
57
|
+
for (const key of ASSEMBLY_KEYS) {
|
|
46
58
|
if (!(key in config)) continue
|
|
47
59
|
assembly[key] = config[key]
|
|
48
60
|
delete config[key]
|
|
@@ -55,12 +67,19 @@ function loadConfig (playbook, configSource = './antora-assembler.yml') {
|
|
|
55
67
|
if (['discrete', 'fuse', 'enclose'].indexOf(assembly.sectionMergeStrategy) < 0) {
|
|
56
68
|
assembly.sectionMergeStrategy = 'discrete'
|
|
57
69
|
}
|
|
70
|
+
if (['relative', 'root-relative', 'absolute'].indexOf(assembly.linkReferenceStyle) < 0) {
|
|
71
|
+
assembly.linkReferenceStyle = 'absolute'
|
|
72
|
+
}
|
|
73
|
+
if (['always', 'if-redundant', 'never'].indexOf(assembly.dropExplicitXrefText) < 0) {
|
|
74
|
+
assembly.dropExplicitXrefText = 'never'
|
|
75
|
+
}
|
|
58
76
|
const build = (config.build ??= {})
|
|
59
77
|
if (build.dir === '$' + '{playbook.output.dir}') {
|
|
60
78
|
throw new Error('Not implemented')
|
|
61
79
|
}
|
|
62
80
|
build.dir &&= expandPath(build.dir, { dot: playbook.dir })
|
|
63
|
-
|
|
81
|
+
// used as cwd of command (and any scripts it requires)
|
|
82
|
+
build.cwd = build.cwd == null ? playbook.dir : expandPath(build.cwd, { dot: playbook.dir })
|
|
64
83
|
if (!('clean' in build) && 'output' in playbook) build.clean = playbook.output.clean
|
|
65
84
|
if (!('publish' in build)) build.publish = true
|
|
66
85
|
if ('keepAggregateSource' in build) {
|
|
@@ -70,6 +89,14 @@ function loadConfig (playbook, configSource = './antora-assembler.yml') {
|
|
|
70
89
|
if (!build.processLimit) {
|
|
71
90
|
build.processLimit = 'processLimit' in build ? Infinity : Math.round(os.cpus().length * 0.5)
|
|
72
91
|
}
|
|
92
|
+
if ('stderr' in build) {
|
|
93
|
+
if (build.stderr === 'log') {
|
|
94
|
+
build.stderr = 'buffer'
|
|
95
|
+
build.stderrSink = 'log'
|
|
96
|
+
} else if (!['ignore', 'print'].includes(build.stderr)) {
|
|
97
|
+
delete build.stderr
|
|
98
|
+
}
|
|
99
|
+
}
|
|
73
100
|
return config
|
|
74
101
|
})
|
|
75
102
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const createAsciiDocFile = require('./util/create-asciidoc-file')
|
|
4
|
+
const parseResourceRef = require('./util/parse-resource-ref')
|
|
4
5
|
const path = require('node:path/posix')
|
|
5
6
|
const sanitize = require('./util/sanitize')
|
|
6
7
|
const unconvertInlineAsciiDoc = require('./util/unconvert-inline-asciidoc')
|
|
@@ -8,7 +9,7 @@ const unconvertInlineAsciiDoc = require('./util/unconvert-inline-asciidoc')
|
|
|
8
9
|
const AttributeEntryRx = /^:([^:-][^:]*):(?: .*)?$/
|
|
9
10
|
const BuiltInNamedEntities = { amp: '&', apos: "'", gt: '>', lt: '<', nbsp: ' ', quot: '"' }
|
|
10
11
|
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(' ')
|
|
12
|
+
const DiscardAttributes = 'doctype leveloffset assembly-navtitle assembly-style underscore'.split(' ')
|
|
12
13
|
const ReservedIdNames = 'content header footnotes footer footer-text premable toc toctitle'.split(' ')
|
|
13
14
|
|
|
14
15
|
function produceAssemblyFile (
|
|
@@ -24,11 +25,10 @@ function produceAssemblyFile (
|
|
|
24
25
|
const pagesByUrl = files.reduce((map, it) => (it.src.family === 'page' ? map.set(it.pub.url, it) : map), new Map())
|
|
25
26
|
if (outline.urlType === 'internal' && !pagesByUrl.get(outline.url) && !(outline.items || []).length) return
|
|
26
27
|
const pagesInOutline = selectPagesInOutline(outline, pagesByUrl, componentVersion)
|
|
27
|
-
const navtitle = outline.content
|
|
28
28
|
const buffer = mergeAsciiDoc(
|
|
29
29
|
loadAsciiDoc,
|
|
30
30
|
contentCatalog,
|
|
31
|
-
buildAsciiDocHeader(componentVersion,
|
|
31
|
+
buildAsciiDocHeader(componentVersion, outline.content, assemblyModel),
|
|
32
32
|
componentVersion,
|
|
33
33
|
outline,
|
|
34
34
|
files,
|
|
@@ -38,7 +38,7 @@ function produceAssemblyFile (
|
|
|
38
38
|
assemblyModel
|
|
39
39
|
)
|
|
40
40
|
const rootLevel = assemblyModel.rootLevel
|
|
41
|
-
const stem = rootLevel === 0 ? 'index' : generateSlug(navtitle)
|
|
41
|
+
const stem = rootLevel === 0 ? 'index' : generateSlug(buffer.navtitle)
|
|
42
42
|
const downloadStem = [componentVersion.name, componentVersion.version, rootLevel === 0 ? '' : stem]
|
|
43
43
|
.filter((it) => it)
|
|
44
44
|
.join('-')
|
|
@@ -63,7 +63,7 @@ function buildAsciiDocHeader (componentVersion, navtitle, assemblyModel) {
|
|
|
63
63
|
let doctitle = navtitleAsciiDoc
|
|
64
64
|
if (navtitlePlain !== componentVersion.title) doctitle = `${componentVersion.title}: ${doctitle}`
|
|
65
65
|
const version = componentVersion.version === 'master' ? '' : componentVersion.version
|
|
66
|
-
|
|
66
|
+
const buffer = [
|
|
67
67
|
`= ${doctitle}`,
|
|
68
68
|
...(version ? [`:revnumber: ${version}`] : []),
|
|
69
69
|
...(doctype === 'article' ? [] : [`:doctype: ${doctype ?? 'book'}`]),
|
|
@@ -75,16 +75,15 @@ function buildAsciiDocHeader (componentVersion, navtitle, assemblyModel) {
|
|
|
75
75
|
`:page-component-display-version: ${componentVersion.displayVersion}`,
|
|
76
76
|
`:page-component-title: ${componentVersion.title}`,
|
|
77
77
|
]
|
|
78
|
+
return Object.assign(buffer, { navtitle })
|
|
78
79
|
}
|
|
79
80
|
|
|
80
81
|
function selectPagesInOutline (outlineEntry, pagesByUrl, componentVersion, accum) {
|
|
81
82
|
accum ??= Object.assign(new Map(), { assembled: { pages: new Map(), assets: new Set() } })
|
|
82
83
|
const page = outlineEntry.urlType === 'internal' ? pagesByUrl.get(outlineEntry.url) : undefined
|
|
83
84
|
if (page) {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
87
|
-
accum.set(page.pub.url, page)
|
|
85
|
+
accum.set(createResourceKey(page.src), page)
|
|
86
|
+
accum.set(outlineEntry.url, page)
|
|
88
87
|
}
|
|
89
88
|
for (const item of outlineEntry.items || []) selectPagesInOutline(item, pagesByUrl, componentVersion, accum)
|
|
90
89
|
return accum
|
|
@@ -106,39 +105,56 @@ function mergeAsciiDoc (
|
|
|
106
105
|
supportsParts = false
|
|
107
106
|
) {
|
|
108
107
|
// 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
|
-
|
|
108
|
+
if (outlineEntry.hash) {
|
|
109
|
+
buffer.inBody ??= false
|
|
110
|
+
return buffer
|
|
111
|
+
}
|
|
112
|
+
let navtitle = outlineEntry.content
|
|
113
|
+
let navtitlePlain = sanitize(navtitle)
|
|
114
|
+
let navtitleAsciiDoc = unconvertInlineAsciiDoc(navtitle)
|
|
115
|
+
const { items = [], unresolved, urlType, url } = outlineEntry
|
|
116
|
+
const {
|
|
117
|
+
doctype,
|
|
118
|
+
filetype,
|
|
119
|
+
embedReferenceStyle: embedRefStyle,
|
|
120
|
+
linkReferenceStyle: linkRefStyle,
|
|
121
|
+
outDirname,
|
|
122
|
+
siteRoot,
|
|
123
|
+
xmlIds,
|
|
124
|
+
} = assemblyModel
|
|
125
|
+
// FIXME: ideally, resource ID would be stored in navigation so we can look up the page more efficiently
|
|
126
|
+
const page = urlType === 'internal' && !unresolved ? pagesInOutline.get(url) : undefined
|
|
111
127
|
const atDocumentRoot = !buffer.inBody
|
|
112
|
-
const atBookRoot = atDocumentRoot && !level &&
|
|
128
|
+
const atBookRoot = atDocumentRoot && !level && doctype === 'book' && (supportsParts = true)
|
|
113
129
|
const hasItems = items.length > 0
|
|
114
|
-
const
|
|
115
|
-
const
|
|
116
|
-
const siteUrl = ((val) => {
|
|
117
|
-
if (!val || val === '/') return ''
|
|
118
|
-
return val.charAt(val.length - 1) === '/' ? val.slice(0, val.length - 1) : val
|
|
119
|
-
})(asciidocConfig.attributes['primary-site-url'] || asciidocConfig.attributes['site-url'])
|
|
120
|
-
const idSeparator = assemblyModel.xmlIds ? '-' : ':'
|
|
130
|
+
const pubRoot = outDirname ? '/' + outDirname : ''
|
|
131
|
+
const idSeparator = xmlIds ? '-' : ':'
|
|
121
132
|
const idScopeSeparator = idSeparator.repeat(3)
|
|
122
133
|
const idCoordinateSeparator = idSeparator === '-' ? '----' : idSeparator
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
contents = Buffer.from(
|
|
133
|
-
contents
|
|
134
|
-
.toString()
|
|
135
|
-
.replace(/^(?:[ \t]*\r\n?|[ \t]*\n)+/, '')
|
|
136
|
-
.trimRight()
|
|
134
|
+
if (page && !pagesInOutline.assembled.pages.has(page)) {
|
|
135
|
+
const contents = page.src.contents
|
|
136
|
+
if (contents == null) {
|
|
137
|
+
buffer.inBody ??= false
|
|
138
|
+
return buffer
|
|
139
|
+
}
|
|
140
|
+
const { component, version, module: module_, relative, origin, mediaType } = page.src
|
|
141
|
+
const pageAsAsciiDoc = new page.constructor(
|
|
142
|
+
Object.assign({}, page, { contents: trimAsciiDoc(contents), mediaType })
|
|
137
143
|
)
|
|
138
|
-
const { component, version, module: module_, relative, origin } = page.src
|
|
139
|
-
const topicPrefix = ~relative.indexOf('/') ? path.dirname(relative) + '/' : ''
|
|
140
|
-
const pageAsAsciiDoc = new page.constructor(Object.assign({}, page, { contents, mediaType: page.src.mediaType }))
|
|
141
144
|
const doc = loadAsciiDoc(pageAsAsciiDoc, contentCatalog, asciidocConfig)
|
|
145
|
+
if (doc.hasAttribute('assembly-navtitle')) {
|
|
146
|
+
navtitleAsciiDoc = doc.getAttribute('assembly-navtitle')
|
|
147
|
+
navtitlePlain = sanitize((navtitle = doc.$apply_reftext_subs(navtitleAsciiDoc)))
|
|
148
|
+
if (buffer.inBody == null) {
|
|
149
|
+
buffer.navtitle = navtitle
|
|
150
|
+
// Q do we need to assert !level here?
|
|
151
|
+
if (assemblyModel.rootLevel) {
|
|
152
|
+
let doctitle = navtitleAsciiDoc
|
|
153
|
+
if (navtitlePlain !== componentVersion.title) doctitle = `${componentVersion.title}: ${doctitle}`
|
|
154
|
+
buffer[0] = `= ${doctitle}`
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
142
158
|
if (atDocumentRoot) {
|
|
143
159
|
const authors = doc.getAuthors()
|
|
144
160
|
if (authors.length) {
|
|
@@ -153,18 +169,7 @@ function mergeAsciiDoc (
|
|
|
153
169
|
}
|
|
154
170
|
// NOTE: in Antora, docname is relative src path from module without file extension
|
|
155
171
|
const docname = doc.getAttribute('docname')
|
|
156
|
-
const
|
|
157
|
-
const qualifyId = component !== componentVersion.name
|
|
158
|
-
let idScope = docnameForId
|
|
159
|
-
let idPrefix
|
|
160
|
-
if (qualifyId) {
|
|
161
|
-
idScope = [component, module_ === 'ROOT' ? '' : module_, idScope].join(idCoordinateSeparator)
|
|
162
|
-
} else if (module_ !== 'ROOT') {
|
|
163
|
-
idScope = module_ + idCoordinateSeparator + idScope
|
|
164
|
-
} else if (ReservedIdNames.includes(docnameForId)) {
|
|
165
|
-
idScope = idPrefix = idScope + idScopeSeparator
|
|
166
|
-
}
|
|
167
|
-
idPrefix ??= idScope + idScopeSeparator
|
|
172
|
+
const { idPrefix, id: idScope } = generateId(page.src, componentVersion, idCoordinateSeparator, idScopeSeparator)
|
|
168
173
|
let pageFragment = ''
|
|
169
174
|
let pageRoles = ''
|
|
170
175
|
let pageStyle = doc.getAttribute('assembly-style', '')
|
|
@@ -198,7 +203,6 @@ function mergeAsciiDoc (
|
|
|
198
203
|
}
|
|
199
204
|
buffer.push(`:page-module: ${module_}`)
|
|
200
205
|
buffer.push(`:page-relative-src-path: ${relative}`)
|
|
201
|
-
//buffer.push(`:page-origin-type: ${origin.type}`)
|
|
202
206
|
buffer.push(`:page-origin-url: ${origin.url}`)
|
|
203
207
|
buffer.push(`:page-origin-start-path:${origin.startPath && ' '}${origin.startPath}`)
|
|
204
208
|
buffer.push(`:page-origin-refname: ${origin.branch || origin.tag}`)
|
|
@@ -371,63 +375,59 @@ function mergeAsciiDoc (
|
|
|
371
375
|
if (~line.indexOf('xref:')) {
|
|
372
376
|
// Q: should we allow : as first character of target?
|
|
373
377
|
line = line.replace(/(?<![\\+])xref:((?:\.\/)?[\p{Alpha}0-9_/.{#].*?)\[(|.*?[^\\])\]/gu, (m, target, text) => {
|
|
374
|
-
let
|
|
378
|
+
let fragment, resource, resourceRef
|
|
375
379
|
const hashIdx = target.indexOf('#')
|
|
376
380
|
if (~hashIdx) {
|
|
377
|
-
|
|
381
|
+
resourceRef = target.slice(0, hashIdx)
|
|
378
382
|
fragment = target.slice(hashIdx + 1)
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
} else if (target.endsWith('.adoc')) {
|
|
382
|
-
pagePart = target
|
|
383
|
+
} else if (target.endsWith('.adoc') || ~target.indexOf('$')) {
|
|
384
|
+
resourceRef = target
|
|
383
385
|
fragment = ''
|
|
384
386
|
} else {
|
|
385
387
|
fragment = target
|
|
386
388
|
}
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
if (siteUrl && (targetPage = contentCatalog.resolvePage(pagePart, page.src)) && targetPage.out) {
|
|
395
|
-
text ||= targetPage.asciidoc?.xreftext || target
|
|
396
|
-
return `${siteUrl}${targetPage.pub.url}${fragment && '#' + fragment}[${text}]`
|
|
389
|
+
// Q: should we validate the internal ID here?
|
|
390
|
+
if (!resourceRef) return `xref:${idPrefix}${fragment}[${text}]`
|
|
391
|
+
const resourceId = parseResourceRef(resourceRef, page.src, 'page', contentCatalog)
|
|
392
|
+
if (resourceId.family !== 'page') {
|
|
393
|
+
if (siteRoot && (resource = contentCatalog.getById(resourceId))?.pub) {
|
|
394
|
+
text ||= resource.asciidoc?.xreftext || target
|
|
395
|
+
return `${resolveLinkTarget(resource, siteRoot, pubRoot, linkRefStyle)}${fragment && '#' + fragment}[${text}]`
|
|
397
396
|
}
|
|
398
|
-
// TODO: handle unresolved
|
|
397
|
+
// TODO: handle unresolved resource better
|
|
399
398
|
return m
|
|
400
399
|
}
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
pagePart = pagePart.slice(colonIdx + 1)
|
|
406
|
-
} else {
|
|
407
|
-
targetModule = module_
|
|
408
|
-
}
|
|
409
|
-
if (pagePart.startsWith('./')) pagePart = topicPrefix + pagePart.slice(2)
|
|
410
|
-
const pageResourceRef = targetModule === 'ROOT' ? pagePart : `${targetModule}:${pagePart}`
|
|
411
|
-
if (!(targetPage = pagesInOutline.get(pageResourceRef))) {
|
|
412
|
-
if (siteUrl && (targetPage = contentCatalog.resolvePage(pageResourceRef, page.src)) && targetPage.out) {
|
|
413
|
-
text ||= targetPage.asciidoc?.xreftext || target
|
|
414
|
-
return `${siteUrl}${targetPage.pub.url}${fragment && '#' + fragment}[${text}]`
|
|
400
|
+
if (!(resource = pagesInOutline.get(createResourceKey(resourceId)))) {
|
|
401
|
+
if (siteRoot && (resource = contentCatalog.getById(resourceId))?.pub) {
|
|
402
|
+
text ||= resource.asciidoc?.xreftext || target
|
|
403
|
+
return `${resolveLinkTarget(resource, siteRoot, pubRoot, linkRefStyle)}${fragment && '#' + fragment}[${text}]`
|
|
415
404
|
}
|
|
416
405
|
// TODO: handle unresolved page better
|
|
417
406
|
return m
|
|
418
407
|
}
|
|
419
|
-
if (
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
408
|
+
if (fragment === resource.asciidoc.id) fragment = ''
|
|
409
|
+
if (
|
|
410
|
+
text &&
|
|
411
|
+
(assemblyModel.dropExplicitXrefText === 'always' ||
|
|
412
|
+
(assemblyModel.dropExplicitXrefText === 'if-redundant' && text === resource.title))
|
|
413
|
+
) {
|
|
414
|
+
text = ''
|
|
415
|
+
}
|
|
416
|
+
const refid = generateId(
|
|
417
|
+
resource.src,
|
|
418
|
+
componentVersion,
|
|
419
|
+
idCoordinateSeparator,
|
|
420
|
+
idScopeSeparator,
|
|
421
|
+
text.length > 0,
|
|
422
|
+
fragment
|
|
423
|
+
).id
|
|
424
|
+
return `xref:${refid}[${text}]`
|
|
425
425
|
})
|
|
426
426
|
}
|
|
427
427
|
if (~line.indexOf('link:{attachmentsdir}/')) {
|
|
428
428
|
line = line.replace(/(?<![\\+])link:\{attachmentsdir\}\/([^\s[]+)\[(|.*?[^\\])\]/g, (m, relative, text) => {
|
|
429
429
|
const attachment =
|
|
430
|
-
|
|
430
|
+
siteRoot &&
|
|
431
431
|
contentCatalog.getById({
|
|
432
432
|
component: componentVersion.name,
|
|
433
433
|
version: componentVersion.version,
|
|
@@ -436,7 +436,7 @@ function mergeAsciiDoc (
|
|
|
436
436
|
relative,
|
|
437
437
|
})
|
|
438
438
|
// TODO: handle unresolved attachment page
|
|
439
|
-
return attachment?.out ? `${
|
|
439
|
+
return attachment?.out ? `${resolveLinkTarget(attachment, siteRoot, pubRoot, linkRefStyle)}[${text}]` : m
|
|
440
440
|
})
|
|
441
441
|
}
|
|
442
442
|
if (~line.indexOf('image:') && !line.startsWith('image::')) {
|
|
@@ -444,9 +444,11 @@ function mergeAsciiDoc (
|
|
|
444
444
|
if (isResourceSpec(target)) {
|
|
445
445
|
const image = contentCatalog.resolveResource(target, page.src, 'image', ['image'])
|
|
446
446
|
// TODO: handle (or report) unresolved image better
|
|
447
|
-
if (image?.out) {
|
|
447
|
+
if (image?.out && (filetype !== 'html' || siteRoot)) {
|
|
448
448
|
pagesInOutline.assembled.assets.add(image)
|
|
449
|
-
return
|
|
449
|
+
return filetype === 'html'
|
|
450
|
+
? `image:${resolveLinkTarget(image, siteRoot, pubRoot, linkRefStyle)}[${attrlist}]`
|
|
451
|
+
: `image:${resolveEmbedTarget(image, outDirname, embedRefStyle, true)}[${attrlist}]`
|
|
450
452
|
}
|
|
451
453
|
}
|
|
452
454
|
return m
|
|
@@ -500,10 +502,13 @@ function mergeAsciiDoc (
|
|
|
500
502
|
if (isResourceSpec(target)) {
|
|
501
503
|
const image = contentCatalog.resolveResource(target, page.src, 'image', ['image'])
|
|
502
504
|
// FIXME: handle (or report) case when image is not resolved
|
|
503
|
-
if (image?.out) {
|
|
504
|
-
const
|
|
505
|
+
if (image?.out && (filetype !== 'html' || siteRoot)) {
|
|
506
|
+
const attrlist = line.slice(line.indexOf('[') + 1, -1)
|
|
505
507
|
pagesInOutline.assembled.assets.add(image)
|
|
506
|
-
lines[idx] =
|
|
508
|
+
lines[idx] =
|
|
509
|
+
filetype === 'html'
|
|
510
|
+
? `${prefix}image::${resolveLinkTarget(image, siteRoot, pubRoot, linkRefStyle, false)}[${attrlist}]`
|
|
511
|
+
: `${prefix}image::${resolveEmbedTarget(image, outDirname, embedRefStyle)}[${attrlist}]`
|
|
507
512
|
}
|
|
508
513
|
}
|
|
509
514
|
lastImageMacroAt = [idx, imageMacroOffset]
|
|
@@ -542,6 +547,7 @@ function mergeAsciiDoc (
|
|
|
542
547
|
}
|
|
543
548
|
} else if (level) {
|
|
544
549
|
if (atDocumentRoot && navtitlePlain === componentVersion.title) {
|
|
550
|
+
buffer.inBody ??= false
|
|
545
551
|
level--
|
|
546
552
|
} else {
|
|
547
553
|
buffer.inBody = true
|
|
@@ -563,9 +569,16 @@ function mergeAsciiDoc (
|
|
|
563
569
|
let sectionTitle = navtitleAsciiDoc
|
|
564
570
|
if (urlType === 'external') {
|
|
565
571
|
sectionTitle = `${url}[${navtitleAsciiDoc.replace(/\]/g, '\\]')}]`
|
|
566
|
-
} else if (urlType === 'internal' && !unresolved
|
|
572
|
+
} else if (urlType === 'internal' && !unresolved) {
|
|
567
573
|
const resource = files.find((it) => it.pub.url === url)
|
|
568
|
-
if (resource)
|
|
574
|
+
if (resource) {
|
|
575
|
+
if (resource.src.family === 'page' && pagesInOutline.has(resource.pub.url)) {
|
|
576
|
+
const refid = generateId(resource.src, componentVersion, idCoordinateSeparator, idScopeSeparator, true).id
|
|
577
|
+
sectionTitle = `xref:${refid}[${navtitleAsciiDoc.replace(/\]/g, '\\]')}]`
|
|
578
|
+
} else if (siteRoot) {
|
|
579
|
+
sectionTitle = `${resolveLinkTarget(resource, siteRoot, pubRoot, linkRefStyle)}[${navtitleAsciiDoc.replace(/\]/g, '\\]')}]`
|
|
580
|
+
}
|
|
581
|
+
}
|
|
569
582
|
}
|
|
570
583
|
let hlevel = level + 1
|
|
571
584
|
if (hlevel > 6) {
|
|
@@ -621,12 +634,8 @@ function processDocumentHeader (doc, lines, buffer, ignoreLines) {
|
|
|
621
634
|
}
|
|
622
635
|
const line = lines[idx]
|
|
623
636
|
if (open === ':' || open === '-:') {
|
|
624
|
-
if (line)
|
|
625
|
-
|
|
626
|
-
if (!line.endsWith(' \\')) open = undefined
|
|
627
|
-
} else {
|
|
628
|
-
open = undefined
|
|
629
|
-
}
|
|
637
|
+
if (open === ':') buffer.push(line)
|
|
638
|
+
if (!line || !line.endsWith(' \\')) open = undefined
|
|
630
639
|
} else if (line) {
|
|
631
640
|
const chr0 = line.charAt()
|
|
632
641
|
let attributeEntryMatch
|
|
@@ -642,7 +651,7 @@ function processDocumentHeader (doc, lines, buffer, ignoreLines) {
|
|
|
642
651
|
} else if (chr0 === ':' && ~line.indexOf(':', 2) && (attributeEntryMatch = line.match(AttributeEntryRx))) {
|
|
643
652
|
const attributeName = attributeEntryMatch[1].replace('!', '')
|
|
644
653
|
if (DiscardAttributes.includes(attributeName)) {
|
|
645
|
-
if (line.endsWith(' \\')) open = '-:'
|
|
654
|
+
if (line.endsWith(' \\')) open = '-:' // disallow value continuation
|
|
646
655
|
} else {
|
|
647
656
|
if (line.endsWith(' \\')) open = ':'
|
|
648
657
|
buffer.push(line)
|
|
@@ -650,6 +659,12 @@ function processDocumentHeader (doc, lines, buffer, ignoreLines) {
|
|
|
650
659
|
} else if (belowDoctitle) {
|
|
651
660
|
if (implicitLines.length === 2 || !/[\p{Alpha}0-9]/u.test(chr0)) break
|
|
652
661
|
implicitLines.push(line)
|
|
662
|
+
} else if (chr0 === '[' && line.charAt(line.length - 1) === ']') {
|
|
663
|
+
const attrlist = line
|
|
664
|
+
.slice(1, -1)
|
|
665
|
+
.trim()
|
|
666
|
+
.replace(/(?:^\w[\w-]*(?!=|[\w-]))?([#.%]\w[\w-]*)*/, '')
|
|
667
|
+
if (attrlist) buffer.push('[' + attrlist + ']')
|
|
653
668
|
}
|
|
654
669
|
} else if (belowDoctitle) {
|
|
655
670
|
break
|
|
@@ -672,7 +687,7 @@ function generateSlug (title) {
|
|
|
672
687
|
return String.fromCharCode(dec ? parseInt(dec, 10) : parseInt(hex, 16))
|
|
673
688
|
})
|
|
674
689
|
.replace(/[\x27\u2019]/g, '')
|
|
675
|
-
.replace(/[^\p{Alpha}0-9
|
|
690
|
+
.replace(/[^\p{Alpha}0-9-]/gu, '-')
|
|
676
691
|
.replace(/^-+|-+$|(-)-+/g, '$1')
|
|
677
692
|
}
|
|
678
693
|
|
|
@@ -739,7 +754,19 @@ function isResourceSpec (str) {
|
|
|
739
754
|
}
|
|
740
755
|
|
|
741
756
|
function getObjectId (obj) {
|
|
742
|
-
return global.Opal.
|
|
757
|
+
return global.Opal.id(obj)
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// NOTE: blank lines at top and bottom of document create mismatch when using line numbers to navigate source lines
|
|
761
|
+
// IMPORTANT: this must not leave behind lines the parser will drop!
|
|
762
|
+
// IDEA: another option is to capture initial lineno of reader and use as offset (but preseves those blank lines)
|
|
763
|
+
function trimAsciiDoc (buffer) {
|
|
764
|
+
return Buffer.from(
|
|
765
|
+
buffer
|
|
766
|
+
.toString()
|
|
767
|
+
.replace(/^(?:[ \t]*\r\n?|[ \t]*\n)+/, '')
|
|
768
|
+
.trimRight()
|
|
769
|
+
)
|
|
743
770
|
}
|
|
744
771
|
|
|
745
772
|
function safePush (onto, entries) {
|
|
@@ -752,4 +779,56 @@ function safePush (onto, entries) {
|
|
|
752
779
|
}
|
|
753
780
|
}
|
|
754
781
|
|
|
782
|
+
function resolveEmbedTarget (resource, outDirname, referenceStyle, escapeForInline) {
|
|
783
|
+
const target =
|
|
784
|
+
referenceStyle === 'output-relative' ? resource.out.path : path.relative(outDirname + '/', resource.out.path)
|
|
785
|
+
return escapeForInline ? target.replace(/_/g, '{underscore}') : target
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
function resolveLinkTarget (resource, siteRoot, pubRoot, referenceStyle, escapeForInline = true) {
|
|
789
|
+
let target
|
|
790
|
+
if (resource.site?.url) {
|
|
791
|
+
target = ['', resource.pub.url]
|
|
792
|
+
} else {
|
|
793
|
+
switch (referenceStyle) {
|
|
794
|
+
case 'absolute':
|
|
795
|
+
target = ['', siteRoot.url + resource.pub.url]
|
|
796
|
+
break
|
|
797
|
+
case 'root-relative':
|
|
798
|
+
target = ['link:', siteRoot.path + resource.pub.url]
|
|
799
|
+
break
|
|
800
|
+
default:
|
|
801
|
+
target = ['link:', computeRelativeUrl(pubRoot + '/', resource.pub.url)]
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
if (escapeForInline) target[1] = target[1].replace(/_/g, '{underscore}')
|
|
805
|
+
return target.join('')
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
function computeRelativeUrl (from, to) {
|
|
809
|
+
const rel = path.relative(from, to)
|
|
810
|
+
return to.charAt(to.length - 1) === '/' ? rel + '/' : rel
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
function createResourceKey ({ component, version, module: mod, family, relative }) {
|
|
814
|
+
return `${version}@${component}:${mod === 'ROOT' ? '' : mod}:${family === 'page' ? '' : family + '$'}${relative}`
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
function generateId (componentSrc, componentVersion, coordinateSep, scopeSep, escapePass, fragment) {
|
|
818
|
+
let { component, module: mod, relative } = componentSrc
|
|
819
|
+
let id = relative.replace(/\.adoc$/, '').replace(/[/.]/g, '-')
|
|
820
|
+
if (component !== componentVersion.name) {
|
|
821
|
+
id = [component, mod === 'ROOT' ? '' : mod, id].join(coordinateSep)
|
|
822
|
+
} else if (mod !== 'ROOT') {
|
|
823
|
+
if (escapePass && coordinateSep === ':' && /(?:pass|stem)$/.test(mod)) mod = mod.replace(/(?:pass|stem)$/, '\\$&')
|
|
824
|
+
id = mod + coordinateSep + id
|
|
825
|
+
} else if (ReservedIdNames.includes(id)) {
|
|
826
|
+
id += scopeSep
|
|
827
|
+
scopeSep = ''
|
|
828
|
+
}
|
|
829
|
+
const idPrefix = id + scopeSep
|
|
830
|
+
if (fragment) id = idPrefix + fragment
|
|
831
|
+
return { idPrefix, id }
|
|
832
|
+
}
|
|
833
|
+
|
|
755
834
|
module.exports = produceAssemblyFile
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
+
const computeOut = require('./util/compute-out')
|
|
3
4
|
const createAsciiDocFile = require('./util/create-asciidoc-file')
|
|
4
5
|
const filterComponentVersions = require('./filter-component-versions')
|
|
5
6
|
const produceAssemblyFile = require('./produce-assembly-file')
|
|
6
7
|
const selectMutableAttributes = require('./select-mutable-attributes')
|
|
7
8
|
|
|
9
|
+
const ATTR_REF_RX = /\\?\{(\w[\w-]*)\}/g
|
|
8
10
|
const IMAGE_MACRO_RX = /^image::?(.+?)\[(.*?)\]$/
|
|
9
11
|
|
|
10
12
|
function produceAssemblyFiles (loadAsciiDoc, contentCatalog, assemblerConfig, resolveAssemblyModel) {
|
|
@@ -16,22 +18,55 @@ function produceAssemblyFiles (loadAsciiDoc, contentCatalog, assemblerConfig, re
|
|
|
16
18
|
sectionMergeStrategy: assemblyConfig.sectionMergeStrategy,
|
|
17
19
|
navigation: componentVersion.navigation,
|
|
18
20
|
xmlIds: assemblyConfig.xmlIds,
|
|
21
|
+
embedReferenceStyle: assemblyConfig.embedReferenceStyle,
|
|
22
|
+
linkReferenceStyle: assemblyConfig.linkReferenceStyle,
|
|
23
|
+
dropExplicitXrefText: assemblyConfig.dropExplicitXrefText,
|
|
19
24
|
})
|
|
20
25
|
const assemblerAsciiDocAttributes = Object.assign({}, assemblerAsciiDocConfig.attributes)
|
|
21
26
|
const { revdate, 'source-highlighter': sourceHighlighter } = assemblerAsciiDocAttributes
|
|
22
27
|
delete assemblerAsciiDocAttributes.revdate
|
|
23
28
|
delete assemblerAsciiDocAttributes['source-highlighter']
|
|
24
29
|
const publishableFiles = contentCatalog.getFiles().filter((file) => file.out)
|
|
30
|
+
let siteRoot
|
|
31
|
+
const configMdc = assemblerConfig.file ? { file: { path: assemblerConfig.file } } : {}
|
|
25
32
|
return filterComponentVersions(contentCatalog.getComponents(), assemblerConfig.componentVersionFilter.names).reduce(
|
|
26
33
|
(accum, componentVersion) => {
|
|
27
34
|
const assemblyModel = resolveAssemblyModel(componentVersion)
|
|
28
35
|
if (!assemblyModel.navigation) return accum
|
|
29
36
|
const { name: componentName, version, title } = componentVersion
|
|
30
37
|
const componentVersionAsciiDocConfig = getAsciiDocConfigWithAsciidoctorReducerExtension(componentVersion)
|
|
38
|
+
const mergedAsciiDocAttributes = collateAsciiDocAttributes(
|
|
39
|
+
Object.assign({ revdate }, componentVersionAsciiDocConfig.attributes),
|
|
40
|
+
assemblerAsciiDocAttributes,
|
|
41
|
+
{ logger: assemblyModel.logger, mdc: configMdc }
|
|
42
|
+
)
|
|
31
43
|
const mergedAsciiDocConfig = Object.assign({}, componentVersionAsciiDocConfig, {
|
|
32
|
-
attributes:
|
|
44
|
+
attributes: mergedAsciiDocAttributes,
|
|
33
45
|
})
|
|
34
|
-
|
|
46
|
+
assemblyModel.outDirname = computeOut.call(contentCatalog, {
|
|
47
|
+
component: componentName,
|
|
48
|
+
version,
|
|
49
|
+
family: 'export',
|
|
50
|
+
relative: '.index.adoc',
|
|
51
|
+
}).dirname
|
|
52
|
+
assemblyModel.filetype = assemblerAsciiDocAttributes['assembler-filetype']
|
|
53
|
+
assemblyModel.siteRoot =
|
|
54
|
+
siteRoot === undefined
|
|
55
|
+
? (siteRoot ??= ((val) => {
|
|
56
|
+
if (!val) return null
|
|
57
|
+
if (val.charAt(val.length - 1) === '/') val = val.slice(0, val.length - 1)
|
|
58
|
+
if (!val || val.charAt() === '/') return { path: val }
|
|
59
|
+
return { url: val, path: extractUrlPath(val) }
|
|
60
|
+
})(mergedAsciiDocAttributes['site-url'] || mergedAsciiDocAttributes['primary-site-url']))
|
|
61
|
+
: siteRoot
|
|
62
|
+
if (assemblyModel.filetype === 'html') {
|
|
63
|
+
let linkRefStyle = assemblyModel.linkReferenceStyle
|
|
64
|
+
if (linkRefStyle === 'absolute' && siteRoot?.url == null) linkRefStyle = 'root-relative'
|
|
65
|
+
if (linkRefStyle === 'root-relative' && siteRoot?.path == null) linkRefStyle = 'relative'
|
|
66
|
+
assemblyModel.linkReferenceStyle = linkRefStyle
|
|
67
|
+
} else {
|
|
68
|
+
assemblyModel.linkReferenceStyle = 'absolute'
|
|
69
|
+
}
|
|
35
70
|
const auxiliaryImages = new Set()
|
|
36
71
|
Object.entries(mergedAsciiDocAttributes).forEach(([name, val]) => {
|
|
37
72
|
const match = name.endsWith('-image') && val.startsWith('image:') && IMAGE_MACRO_RX.exec(val)
|
|
@@ -87,7 +122,6 @@ function produceAssemblyFiles (loadAsciiDoc, contentCatalog, assemblerConfig, re
|
|
|
87
122
|
accum.push(assemblyFile)
|
|
88
123
|
return true
|
|
89
124
|
}, false)
|
|
90
|
-
mergedAsciiDocAttributes.doctype = assemblyModel.doctype
|
|
91
125
|
sourceHighlighter
|
|
92
126
|
? (mergedAsciiDocAttributes['source-highlighter'] = sourceHighlighter)
|
|
93
127
|
: delete mergedAsciiDocAttributes['source-highlighter']
|
|
@@ -143,4 +177,39 @@ function prepareOutlines (navigation, rootEntry, rootLevel) {
|
|
|
143
177
|
return items
|
|
144
178
|
}
|
|
145
179
|
|
|
180
|
+
function collateAsciiDocAttributes (collated, additional, { logger, mdc }) {
|
|
181
|
+
Object.entries(additional).forEach(([name, val]) => {
|
|
182
|
+
if (val && val.constructor === String) {
|
|
183
|
+
let alias
|
|
184
|
+
val = val.replace(ATTR_REF_RX, (ref, refname) => {
|
|
185
|
+
if (ref.charAt() === '\\') return ref.substr(1)
|
|
186
|
+
const refval = collated[refname]
|
|
187
|
+
if (refval == null || refval === false) {
|
|
188
|
+
if (refname in collated && ref === val) {
|
|
189
|
+
alias = refval
|
|
190
|
+
} else if (collated['attribute-missing'] === 'warn') {
|
|
191
|
+
if (logger) {
|
|
192
|
+
logger.warn(mdc, "Skipping reference to missing attribute '%s' in value of '%s' attribute", refname, name)
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return ref
|
|
196
|
+
}
|
|
197
|
+
if (refval.constructor === String) return refval
|
|
198
|
+
if (ref !== val) return refval.toString()
|
|
199
|
+
alias = refval
|
|
200
|
+
return ref
|
|
201
|
+
})
|
|
202
|
+
if (alias !== undefined) val = alias
|
|
203
|
+
}
|
|
204
|
+
collated[name] = val
|
|
205
|
+
})
|
|
206
|
+
return collated
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function extractUrlPath (url) {
|
|
210
|
+
if (!url) return ''
|
|
211
|
+
const urlPath = new URL(url).pathname
|
|
212
|
+
return urlPath === '/' ? '' : urlPath
|
|
213
|
+
}
|
|
214
|
+
|
|
146
215
|
module.exports = produceAssemblyFiles
|
package/lib/util/compute-out.js
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
const { posix: path } = require('node:path')
|
|
4
4
|
|
|
5
5
|
function computeOut (src) {
|
|
6
|
-
const { component, version, module: module_, family, relative } = src
|
|
6
|
+
const { component, version, module: module_ = 'ROOT', 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] : ''
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
function parseResourceRef (ref, ctx = {}, family = undefined, contentCatalog = undefined) {
|
|
4
|
+
const atIdx = ref.indexOf('@')
|
|
5
|
+
let firstColonIdx = ref.indexOf(':')
|
|
6
|
+
let component, version, module_
|
|
7
|
+
if (~atIdx && (~firstColonIdx ? atIdx < firstColonIdx : true)) {
|
|
8
|
+
if ((version = ref.slice(0, atIdx)) === '_') version = ''
|
|
9
|
+
ref = ref.slice(atIdx + 1)
|
|
10
|
+
if (~firstColonIdx) firstColonIdx -= atIdx + 1
|
|
11
|
+
}
|
|
12
|
+
const addColons = ~firstColonIdx ? (~ref.indexOf(':', firstColonIdx + 1) ? '' : ':') : '::'
|
|
13
|
+
const segments = (addColons + ref).split(':')
|
|
14
|
+
if ((component = segments[0])) {
|
|
15
|
+
module_ = segments[1] || 'ROOT'
|
|
16
|
+
version ??= contentCatalog?.getComponent(component)?.latest.version
|
|
17
|
+
} else {
|
|
18
|
+
component = ctx.component
|
|
19
|
+
version ??= ctx.version
|
|
20
|
+
module_ = segments[1] || ctx.module || 'ROOT'
|
|
21
|
+
}
|
|
22
|
+
let relative = segments.length > 3 ? segments.slice(2).join(':') : segments[2]
|
|
23
|
+
const dollarIdx = relative.indexOf('$')
|
|
24
|
+
if (~dollarIdx) {
|
|
25
|
+
family = relative.slice(0, dollarIdx) || family
|
|
26
|
+
relative = relative.slice(dollarIdx + 1)
|
|
27
|
+
}
|
|
28
|
+
if (relative.charAt() === '.' && relative.charAt(1) === '/') {
|
|
29
|
+
const ctxRelative = ctx.relative
|
|
30
|
+
const topic = ctxRelative ? ctxRelative.slice(0, (ctxRelative.lastIndexOf('/') + 1 || 1) - 1) : undefined
|
|
31
|
+
relative = (topic ? topic + '/' : '') + relative.slice(2)
|
|
32
|
+
}
|
|
33
|
+
return { component, version, module: module_, family, relative }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
module.exports = parseResourceRef
|
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.11",
|
|
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)",
|
|
@@ -27,11 +27,13 @@
|
|
|
27
27
|
".": "./lib/index.js",
|
|
28
28
|
"./filter-component-versions": "./lib/filter-component-versions.js",
|
|
29
29
|
"./load-config": "./lib/load-config.js",
|
|
30
|
+
"./parse-resource-ref": "./lib/util/parse-resource-ref.js",
|
|
30
31
|
"./produce-assembly-file": "./lib/produce-assembly-file.js",
|
|
31
32
|
"./produce-assembly-files": "./lib/produce-assembly-files.js",
|
|
32
33
|
"./select-mutable-attributes": "./lib/select-mutable-attributes.js"
|
|
33
34
|
},
|
|
34
35
|
"imports": {
|
|
36
|
+
"#asciidoctor-log-adapter": "./adapters/asciidoctor/jsonl-logger.rb",
|
|
35
37
|
"#run-command": "@antora/run-command-helper",
|
|
36
38
|
"#unconvert-inline-asciidoc": "./lib/util/unconvert-inline-asciidoc.js"
|
|
37
39
|
},
|
|
@@ -51,6 +53,7 @@
|
|
|
51
53
|
"node": ">=16.0.0"
|
|
52
54
|
},
|
|
53
55
|
"files": [
|
|
56
|
+
"adapters/",
|
|
54
57
|
"lib/"
|
|
55
58
|
],
|
|
56
59
|
"keywords": [
|