@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.
@@ -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
@@ -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 targetBackend = converter == null ? undefined : (converter.backend ?? converter.extname?.slice(1))
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
- const { convert = converter, extname: targetExtname, mediaType: targetMediaType } = converter ?? {}
52
- if (!(convert && assemblyFiles.length)) return assemblyFiles
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, contentCatalog, buildConfig)
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 convertAttributes = prepareConvertAttributes(doc, targetExtname, buildConfig)
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
- (fileOrContents = new LazyReadable(() => fs.createReadStream(convertAttributes.outfile))) => {
63
- return coerceToExportFormat(doc, targetBackend, targetExtname, targetMediaType, fileOrContents)
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
- ;(assemblerMeta.exports ??= []).push({ fragment, file })
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, defaults, intrinsicAttributes, navigationCatalog) {
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 { insertStartPage, rootLevel, sectionMergeStrategy, navigation }
133
+ return Object.assign({ logger }, common, { navigation })
110
134
  }
111
135
  }
112
- const boundSendToLog = sendToLog.bind(context.getLogger ? context.getLogger(PACKAGE_NAME) : undefined)
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 { navFiles, messages, ...model } = Object.assign(
117
- { insertStartPage, rootLevel, sectionMergeStrategy },
118
- componentVersionProfiles?.get(intrinsicAttributes['assembler-profile']) ?? componentVersionProfiles?.get()
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 docdir = dir
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: ospath.dirname(docfile),
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('-a', val)
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, contentCatalog, buildConfig) {
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 p in obj) return true
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
- this.updateVariables(assemblerProfiles ? undefined : { assemblerProfiles: getAssemblerProfiles(contentCatalog) })
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
- let assemblerConfig = descriptor.ext?.assembler
68
+ const assemblerConfig = descriptor.ext?.assembler
68
69
  if (!assemblerConfig) return
69
- if (Array.isArray(assemblerConfig)) {
70
- if (!assemblerConfig.length) return
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) {
@@ -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 ? fsp.readFile(configSource).then((data) => camelCaseKeys(yaml.load(data), ['asciidoc'])) : {}
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 ['rootLevel', 'insertStartPage', 'sectionMergeStrategy']) {
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
- build.cwd = playbook.dir // use playbook.dir for finding and loading require scripts
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, navtitle, assemblyModel),
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
- return [
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
- if (page.src.component === componentVersion.name && page.src.version === componentVersion.version) {
85
- accum.set(`${page.src.module === 'ROOT' ? '' : page.src.module + ':'}${page.src.relative}`, page)
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) return buffer
110
- const { content: navtitle, items = [], unresolved, urlType, url } = outlineEntry
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 && assemblyModel.doctype === 'book' && (supportsParts = true)
128
+ const atBookRoot = atDocumentRoot && !level && doctype === 'book' && (supportsParts = true)
113
129
  const hasItems = items.length > 0
114
- const navtitlePlain = sanitize(navtitle)
115
- const navtitleAsciiDoc = unconvertInlineAsciiDoc(navtitle)
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
- // FIXME: ideally, resource ID would be stored in navigation so we can look up the page more efficiently
124
- let page = urlType === 'internal' && !unresolved ? pagesInOutline.get(url) : undefined
125
- if (page && pagesInOutline.assembled.pages.has(page)) page = undefined
126
- if (page) {
127
- let contents = page.src.contents
128
- if (contents == null) return buffer
129
- // NOTE: blank lines at top and bottom of document create mismatch when using line numbers to navigate source lines
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()
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 docnameForId = docname.replace(/[/.]/g, '-')
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 pagePart, fragment, targetPage
378
+ let fragment, resource, resourceRef
375
379
  const hashIdx = target.indexOf('#')
376
380
  if (~hashIdx) {
377
- pagePart = target.slice(0, hashIdx)
381
+ resourceRef = target.slice(0, hashIdx)
378
382
  fragment = target.slice(hashIdx + 1)
379
- // TODO: for now, assume .adoc; in the future, consider other file extensions
380
- if (pagePart && !pagePart.endsWith('.adoc')) pagePart += '.adoc'
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
- if (!pagePart) {
388
- // Q: should we validate the internal ID here?
389
- return text && ~text.indexOf('=')
390
- ? `xref:${idPrefix}${fragment}[${text}]`
391
- : `<<${idPrefix}${fragment}${text ? ',' + text.replace(/\\]/g, ']') : ''}>>`
392
- }
393
- if (~pagePart.indexOf('@') || /:.*:/.test(pagePart)) {
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 page better
397
+ // TODO: handle unresolved resource better
399
398
  return m
400
399
  }
401
- let targetModule
402
- const colonIdx = pagePart.indexOf(':')
403
- if (~colonIdx) {
404
- targetModule = pagePart.slice(0, colonIdx)
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 (targetModule !== 'ROOT') pagePart = `${targetModule}${idCoordinateSeparator}${pagePart}`
420
- pagePart = pagePart.replace(/\.adoc$/, '').replace(/[/.]/g, '-')
421
- const refid = fragment
422
- ? `${pagePart}${idScopeSeparator}${fragment}`
423
- : pagePart + (ReservedIdNames.includes(pagePart) ? idScopeSeparator : '')
424
- return `<<${refid}${text && text !== targetPage.title ? ',' + text.replace(/\\]/g, ']') : ''}>>`
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
- siteUrl &&
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 ? `${siteUrl}${attachment.pub.url.replace(/_/g, '{underscore}')}[${text}]` : m
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 `image:${image.out.path.replace(/_/g, '{underscore}')}[${attrlist}]`
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 boxedAttrlist = line.slice(line.indexOf('['))
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] = `${prefix}image::${image.out.path}${boxedAttrlist}`
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 && siteUrl) {
572
+ } else if (urlType === 'internal' && !unresolved) {
567
573
  const resource = files.find((it) => it.pub.url === url)
568
- if (resource) sectionTitle = `${siteUrl}${resource.pub.url}[${navtitleAsciiDoc.replace(/\]/g, '\\]')}]`
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
- if (open === ':') buffer.push(line)
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\-]/gu, '-')
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.uid()
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: Object.assign({ revdate }, componentVersionAsciiDocConfig.attributes, assemblerAsciiDocAttributes),
44
+ attributes: mergedAsciiDocAttributes,
33
45
  })
34
- const mergedAsciiDocAttributes = mergedAsciiDocConfig.attributes
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
@@ -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, ext: extname, name: stem } = path.parse(outRelative)
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 depth of startPage.out.moduleRootPath.split('/')) outPathSegments.pop()
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] : ''
@@ -1,7 +1,6 @@
1
1
  'use strict'
2
2
 
3
3
  const computeOut = require('./compute-out')
4
- const { posix: path } = require('node:path')
5
4
 
6
5
  function createAsciiDocFile (contentCatalog, file) {
7
6
  file.mediaType = 'text/asciidoc'
@@ -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.1",
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": [