@antora/assembler 1.0.0-beta.5 → 1.0.0-beta.7

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.
@@ -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 (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()
@@ -99,23 +117,24 @@ async function assembleContent (playbook, contentCatalog, converter, { configSou
99
117
  })
100
118
  }
101
119
 
102
- function createResolveAssemblyModel (context, contentCatalog, shared, intrinsicAttributes, navigationCatalog) {
120
+ function createResolveAssemblyModel (context, contentCatalog, common, intrinsicAttributes, navigationCatalog) {
121
+ const logger = context.getLogger?.(PACKAGE_NAME)
103
122
  const { assemblerProfiles } = context.getVariables()
104
123
  if (!assemblerProfiles) {
105
124
  return (componentVersion) => {
106
125
  const navigation =
107
126
  navigationCatalog?.getNavigation(componentVersion.name, componentVersion.version) ?? componentVersion.navigation
108
- return Object.assign({}, shared, { navigation })
127
+ return Object.assign({ logger }, common, { navigation })
109
128
  }
110
129
  }
111
- const boundSendToLog = sendToLog.bind(context.getLogger ? context.getLogger(PACKAGE_NAME) : undefined)
130
+ const boundSendToLog = sendToLog.bind(logger)
112
131
  const { buildNavigation = require('@antora/navigation-builder') } = context.getFunctions()
113
132
  return (componentVersion) => {
114
133
  const componentVersionProfiles = assemblerProfiles.get(componentVersion.version + '@' + componentVersion.name)
115
134
  const overrides =
116
135
  componentVersionProfiles?.get(intrinsicAttributes['assembler-profile']) ?? componentVersionProfiles?.get() ?? {}
117
136
  const { navFiles, messages } = overrides
118
- const model = Object.assign({}, shared, overrides)
137
+ const model = Object.assign({ logger }, common, overrides)
119
138
  delete model.navFiles
120
139
  delete model.messages
121
140
  const navigationOverride = navigationCatalog?.getNavigation(componentVersion.name, componentVersion.version)
@@ -144,7 +163,7 @@ function createResolveAssemblyModel (context, contentCatalog, shared, intrinsicA
144
163
  }
145
164
  }
146
165
 
147
- function prepareConvertAttributes (doc, targetExtname, buildConfig) {
166
+ function prepareConvertAttributes (doc, targetExtname, relativeToOutput, buildConfig) {
148
167
  const {
149
168
  asciidoc: { attributes: docAttributes } = { attributes: {} },
150
169
  extname: docfilesuffix,
@@ -154,7 +173,8 @@ function prepareConvertAttributes (doc, targetExtname, buildConfig) {
154
173
  const { cwd = process.cwd(), dir = cwd } = buildConfig
155
174
  const docname = family + '$' + relative.slice(0, relative.length - docfilesuffix.length)
156
175
  const docfile = ospath.join(dir, reldocfile)
157
- const docdir = dir
176
+ const outdir = ospath.dirname(docfile)
177
+ const docdir = relativeToOutput ? dir : outdir
158
178
  const imagesdir = ''
159
179
  const outfile = docfile.slice(0, docfile.length - docfilesuffix.length) + targetExtname
160
180
  const attributes = Object.assign({}, docAttributes, {
@@ -163,7 +183,7 @@ function prepareConvertAttributes (doc, targetExtname, buildConfig) {
163
183
  docfilesuffix,
164
184
  'docname@': docname,
165
185
  imagesdir,
166
- outdir: ospath.dirname(docfile),
186
+ outdir,
167
187
  outfile,
168
188
  outfilesuffix: targetExtname,
169
189
  toArgs (optionFlag, command) {
@@ -177,7 +197,7 @@ function prepareConvertAttributes (doc, targetExtname, buildConfig) {
177
197
  } else {
178
198
  val = `!${name}${val === false ? '@' : ''}`
179
199
  }
180
- args.push('-a', val)
200
+ args.push(optionFlag, val)
181
201
  }
182
202
  return args
183
203
  },
@@ -206,7 +226,7 @@ function findFirstExportWithExtname (extname) {
206
226
  }
207
227
 
208
228
  // TODO: if no workspace dir is defined, we shouldn't continue
209
- function prepareWorkspace (publishFiles, assemblyFiles, contentCatalog, buildConfig) {
229
+ function prepareWorkspace (publishFiles, assemblyFiles, buildConfig) {
210
230
  const { dir, clean, keepSource } = buildConfig
211
231
  const files = []
212
232
  const outPaths = new Set()
@@ -221,9 +241,42 @@ function prepareWorkspace (publishFiles, assemblyFiles, contentCatalog, buildCon
221
241
  return publishFiles({ output: { clean, dir } }, { getFiles: () => files })
222
242
  }
223
243
 
244
+ function resolveFileOrContents (convertResult, convertAttributes, buildConfig, loggerName) {
245
+ let fileOrContents
246
+ if (convertResult?.status != null) {
247
+ fileOrContents = 'file' in convertResult ? convertResult.file : convertResult.contents
248
+ let logger, match
249
+ if (buildConfig.stderrSink === 'log' && convertResult.stderr.length && (logger = this.getLogger(loggerName))) {
250
+ const docfile = convertAttributes.docfile
251
+ const command = buildConfig.command
252
+ const stderr = convertResult.stderr.toString().trimEnd()
253
+ stderr.split(NEWLINE_RX).forEach((line) => {
254
+ const ctx = { command, file: { path: docfile } }
255
+ if (line.charAt() === '{' && line.charAt(line.length - 1) === '}') {
256
+ const entry = JSON.parse(line)
257
+ if (entry.name) ctx.program = entry.name
258
+ if (entry.file?.line) ctx.line = entry.file.line
259
+ logger[entry.level](ctx, entry.msg)
260
+ } else if ((match = /^(.+):(\d+): warning: (.+)/.exec(line))) {
261
+ const [, scriptPath, lineno, msg] = match
262
+ ctx.stack = [{ file: { path: scriptPath }, line: parseInt(lineno, 10) }]
263
+ logger.warn(ctx, msg)
264
+ } else {
265
+ logger.info(ctx, line)
266
+ }
267
+ })
268
+ }
269
+ } else if (convertResult !== undefined) {
270
+ return convertResult
271
+ }
272
+ return fileOrContents === undefined
273
+ ? new LazyReadable(() => fs.createReadStream(convertAttributes.outfile))
274
+ : fileOrContents
275
+ }
276
+
224
277
  function isBound (obj) {
225
278
  if (obj == null) return false
226
- for (const p in obj) return true
279
+ for (const _ in obj) return true
227
280
  return false
228
281
  }
229
282
 
@@ -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,6 +67,12 @@ 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')
@@ -70,6 +88,14 @@ function loadConfig (playbook, configSource = './antora-assembler.yml') {
70
88
  if (!build.processLimit) {
71
89
  build.processLimit = 'processLimit' in build ? Infinity : Math.round(os.cpus().length * 0.5)
72
90
  }
91
+ if ('stderr' in build) {
92
+ if (build.stderr === 'log') {
93
+ build.stderr = 'buffer'
94
+ build.stderrSink = 'log'
95
+ } else if (!['ignore', 'print'].includes(build.stderr)) {
96
+ delete build.stderr
97
+ }
98
+ }
73
99
  return config
74
100
  })
75
101
  }
@@ -8,7 +8,7 @@ const unconvertInlineAsciiDoc = require('./util/unconvert-inline-asciidoc')
8
8
  const AttributeEntryRx = /^:([^:-][^:]*):(?: .*)?$/
9
9
  const BuiltInNamedEntities = { amp: '&', apos: "'", gt: '>', lt: '<', nbsp: ' ', quot: '"' }
10
10
  const CharRefRx = /&(?:([a-z][a-z]+\d{0,2})|#(?:(\d{2,6})|x([a-z\d]{2,5})));/g
11
- const DiscardAttributes = 'doctype leveloffset assembly-style underscore'.split(' ')
11
+ const DiscardAttributes = 'doctype leveloffset assembly-navtitle assembly-style underscore'.split(' ')
12
12
  const ReservedIdNames = 'content header footnotes footer footer-text premable toc toctitle'.split(' ')
13
13
 
14
14
  function produceAssemblyFile (
@@ -24,11 +24,10 @@ function produceAssemblyFile (
24
24
  const pagesByUrl = files.reduce((map, it) => (it.src.family === 'page' ? map.set(it.pub.url, it) : map), new Map())
25
25
  if (outline.urlType === 'internal' && !pagesByUrl.get(outline.url) && !(outline.items || []).length) return
26
26
  const pagesInOutline = selectPagesInOutline(outline, pagesByUrl, componentVersion)
27
- const navtitle = outline.content
28
27
  const buffer = mergeAsciiDoc(
29
28
  loadAsciiDoc,
30
29
  contentCatalog,
31
- buildAsciiDocHeader(componentVersion, navtitle, assemblyModel),
30
+ buildAsciiDocHeader(componentVersion, outline.content, assemblyModel),
32
31
  componentVersion,
33
32
  outline,
34
33
  files,
@@ -38,7 +37,7 @@ function produceAssemblyFile (
38
37
  assemblyModel
39
38
  )
40
39
  const rootLevel = assemblyModel.rootLevel
41
- const stem = rootLevel === 0 ? 'index' : generateSlug(navtitle)
40
+ const stem = rootLevel === 0 ? 'index' : generateSlug(buffer.navtitle)
42
41
  const downloadStem = [componentVersion.name, componentVersion.version, rootLevel === 0 ? '' : stem]
43
42
  .filter((it) => it)
44
43
  .join('-')
@@ -63,7 +62,7 @@ function buildAsciiDocHeader (componentVersion, navtitle, assemblyModel) {
63
62
  let doctitle = navtitleAsciiDoc
64
63
  if (navtitlePlain !== componentVersion.title) doctitle = `${componentVersion.title}: ${doctitle}`
65
64
  const version = componentVersion.version === 'master' ? '' : componentVersion.version
66
- return [
65
+ const buffer = [
67
66
  `= ${doctitle}`,
68
67
  ...(version ? [`:revnumber: ${version}`] : []),
69
68
  ...(doctype === 'article' ? [] : [`:doctype: ${doctype ?? 'book'}`]),
@@ -75,6 +74,7 @@ function buildAsciiDocHeader (componentVersion, navtitle, assemblyModel) {
75
74
  `:page-component-display-version: ${componentVersion.displayVersion}`,
76
75
  `:page-component-title: ${componentVersion.title}`,
77
76
  ]
77
+ return Object.assign(buffer, { navtitle })
78
78
  }
79
79
 
80
80
  function selectPagesInOutline (outlineEntry, pagesByUrl, componentVersion, accum) {
@@ -106,39 +106,57 @@ function mergeAsciiDoc (
106
106
  supportsParts = false
107
107
  ) {
108
108
  // TODO: we could try to be smart about it and make sure the page with fragment is included at least once
109
- if (outlineEntry.hash) return buffer
110
- const { content: navtitle, items = [], unresolved, urlType, url } = outlineEntry
109
+ if (outlineEntry.hash) {
110
+ buffer.inBody ??= false
111
+ return buffer
112
+ }
113
+ let navtitle = outlineEntry.content
114
+ let navtitlePlain = sanitize(navtitle)
115
+ let navtitleAsciiDoc = unconvertInlineAsciiDoc(navtitle)
116
+ const { items = [], unresolved, urlType, url } = outlineEntry
117
+ const {
118
+ doctype,
119
+ filetype,
120
+ embedReferenceStyle: embedRefStyle,
121
+ linkReferenceStyle: linkRefStyle,
122
+ outDirname,
123
+ siteRoot,
124
+ xmlIds,
125
+ } = assemblyModel
126
+ // FIXME: ideally, resource ID would be stored in navigation so we can look up the page more efficiently
127
+ const page = urlType === 'internal' && !unresolved ? pagesInOutline.get(url) : undefined
111
128
  const atDocumentRoot = !buffer.inBody
112
- const atBookRoot = atDocumentRoot && !level && assemblyModel.doctype === 'book' && (supportsParts = true)
129
+ const atBookRoot = atDocumentRoot && !level && doctype === 'book' && (supportsParts = true)
113
130
  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 ? '-' : ':'
131
+ const pubRoot = outDirname ? '/' + outDirname : ''
132
+ const idSeparator = xmlIds ? '-' : ':'
121
133
  const idScopeSeparator = idSeparator.repeat(3)
122
134
  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()
137
- )
138
- const { component, version, module: module_, relative, origin } = page.src
135
+ if (page && !pagesInOutline.assembled.pages.has(page)) {
136
+ const contents = page.src.contents
137
+ if (contents == null) {
138
+ buffer.inBody ??= false
139
+ return buffer
140
+ }
141
+ const { component, version, module: module_, relative, origin, mediaType } = page.src
139
142
  const topicPrefix = ~relative.indexOf('/') ? path.dirname(relative) + '/' : ''
140
- const pageAsAsciiDoc = new page.constructor(Object.assign({}, page, { contents, mediaType: page.src.mediaType }))
143
+ const pageAsAsciiDoc = new page.constructor(
144
+ Object.assign({}, page, { contents: trimAsciiDoc(contents), mediaType })
145
+ )
141
146
  const doc = loadAsciiDoc(pageAsAsciiDoc, contentCatalog, asciidocConfig)
147
+ if (doc.hasAttribute('assembly-navtitle')) {
148
+ navtitleAsciiDoc = doc.getAttribute('assembly-navtitle')
149
+ navtitlePlain = sanitize((navtitle = doc.$apply_reftext_subs(navtitleAsciiDoc)))
150
+ if (buffer.inBody == null) {
151
+ buffer.navtitle = navtitle
152
+ // Q do we need to assert !level here?
153
+ if (assemblyModel.rootLevel) {
154
+ let doctitle = navtitleAsciiDoc
155
+ if (navtitlePlain !== componentVersion.title) doctitle = `${componentVersion.title}: ${doctitle}`
156
+ buffer[0] = `= ${doctitle}`
157
+ }
158
+ }
159
+ }
142
160
  if (atDocumentRoot) {
143
161
  const authors = doc.getAuthors()
144
162
  if (authors.length) {
@@ -198,7 +216,6 @@ function mergeAsciiDoc (
198
216
  }
199
217
  buffer.push(`:page-module: ${module_}`)
200
218
  buffer.push(`:page-relative-src-path: ${relative}`)
201
- //buffer.push(`:page-origin-type: ${origin.type}`)
202
219
  buffer.push(`:page-origin-url: ${origin.url}`)
203
220
  buffer.push(`:page-origin-start-path:${origin.startPath && ' '}${origin.startPath}`)
204
221
  buffer.push(`:page-origin-refname: ${origin.branch || origin.tag}`)
@@ -371,7 +388,7 @@ function mergeAsciiDoc (
371
388
  if (~line.indexOf('xref:')) {
372
389
  // Q: should we allow : as first character of target?
373
390
  line = line.replace(/(?<![\\+])xref:((?:\.\/)?[\p{Alpha}0-9_/.{#].*?)\[(|.*?[^\\])\]/gu, (m, target, text) => {
374
- let relativePart, fragment, targetResource, isPage
391
+ let relativePart, fragment, resource, isPage
375
392
  const hashIdx = target.indexOf('#')
376
393
  const dollarIdx = target.indexOf('$')
377
394
  if (~hashIdx) {
@@ -400,9 +417,9 @@ function mergeAsciiDoc (
400
417
  if (~hashIdx && !relativePart.endsWith('.adoc')) relativePart += '.adoc'
401
418
  }
402
419
  if (!isPage || ~relativePart.indexOf('@') || /:.*:/.test(relativePart)) {
403
- if (siteUrl && (targetResource = contentCatalog.resolveResource(relativePart, page.src, 'page'))?.pub) {
404
- text ||= targetResource.asciidoc?.xreftext || target
405
- return `${siteUrl}${targetResource.pub.url}${fragment && '#' + fragment}[${text}]`
420
+ if (siteRoot && (resource = contentCatalog.resolveResource(relativePart, page.src, 'page'))?.pub) {
421
+ text ||= resource.asciidoc?.xreftext || target
422
+ return `${resolveLinkTarget(resource, siteRoot, pubRoot, linkRefStyle)}${fragment && '#' + fragment}[${text}]`
406
423
  }
407
424
  // TODO: handle unresolved resource better
408
425
  return m
@@ -417,14 +434,10 @@ function mergeAsciiDoc (
417
434
  targetModule = module_
418
435
  }
419
436
  const pageResourceRef = targetModule === 'ROOT' ? relativePart : `${targetModule}:${relativePart}`
420
- if (!(targetResource = pagesInOutline.get(pageResourceRef))) {
421
- if (
422
- siteUrl &&
423
- (targetResource = contentCatalog.resolvePage(pageResourceRef, page.src)) &&
424
- targetResource.out
425
- ) {
426
- text ||= targetResource.asciidoc?.xreftext || target
427
- return `${siteUrl}${targetResource.pub.url}${fragment && '#' + fragment}[${text}]`
437
+ if (!(resource = pagesInOutline.get(pageResourceRef))) {
438
+ if (siteRoot && (resource = contentCatalog.resolvePage(pageResourceRef, page.src)) && resource.out) {
439
+ text ||= resource.asciidoc?.xreftext || target
440
+ return `${resolveLinkTarget(resource, siteRoot, pubRoot, linkRefStyle)}${fragment && '#' + fragment}[${text}]`
428
441
  }
429
442
  // TODO: handle unresolved page better
430
443
  return m
@@ -434,13 +447,20 @@ function mergeAsciiDoc (
434
447
  const refid = fragment
435
448
  ? `${relativePart}${idScopeSeparator}${fragment}`
436
449
  : relativePart + (ReservedIdNames.includes(relativePart) ? idScopeSeparator : '')
437
- return `<<${refid}${text && text !== targetResource.title ? ',' + text.replace(/\\]/g, ']') : ''}>>`
450
+ if (
451
+ text &&
452
+ (assemblyModel.dropExplicitXrefText === 'always' ||
453
+ (assemblyModel.dropExplicitXrefText === 'if-redundant' && text === resource.title))
454
+ ) {
455
+ text = ''
456
+ }
457
+ return `<<${refid}${text ? ',' + text.replace(/\\]/g, ']') : ''}>>`
438
458
  })
439
459
  }
440
460
  if (~line.indexOf('link:{attachmentsdir}/')) {
441
461
  line = line.replace(/(?<![\\+])link:\{attachmentsdir\}\/([^\s[]+)\[(|.*?[^\\])\]/g, (m, relative, text) => {
442
462
  const attachment =
443
- siteUrl &&
463
+ siteRoot &&
444
464
  contentCatalog.getById({
445
465
  component: componentVersion.name,
446
466
  version: componentVersion.version,
@@ -449,7 +469,7 @@ function mergeAsciiDoc (
449
469
  relative,
450
470
  })
451
471
  // TODO: handle unresolved attachment page
452
- return attachment?.out ? `${siteUrl}${attachment.pub.url.replace(/_/g, '{underscore}')}[${text}]` : m
472
+ return attachment?.out ? `${resolveLinkTarget(attachment, siteRoot, pubRoot, linkRefStyle)}[${text}]` : m
453
473
  })
454
474
  }
455
475
  if (~line.indexOf('image:') && !line.startsWith('image::')) {
@@ -457,9 +477,11 @@ function mergeAsciiDoc (
457
477
  if (isResourceSpec(target)) {
458
478
  const image = contentCatalog.resolveResource(target, page.src, 'image', ['image'])
459
479
  // TODO: handle (or report) unresolved image better
460
- if (image?.out) {
480
+ if (image?.out && (filetype !== 'html' || siteRoot)) {
461
481
  pagesInOutline.assembled.assets.add(image)
462
- return `image:${image.out.path.replace(/_/g, '{underscore}')}[${attrlist}]`
482
+ return filetype === 'html'
483
+ ? `image:${resolveLinkTarget(image, siteRoot, pubRoot, linkRefStyle)}[${attrlist}]`
484
+ : `image:${resolveEmbedTarget(image, outDirname, embedRefStyle, true)}[${attrlist}]`
463
485
  }
464
486
  }
465
487
  return m
@@ -513,10 +535,13 @@ function mergeAsciiDoc (
513
535
  if (isResourceSpec(target)) {
514
536
  const image = contentCatalog.resolveResource(target, page.src, 'image', ['image'])
515
537
  // FIXME: handle (or report) case when image is not resolved
516
- if (image?.out) {
517
- const boxedAttrlist = line.slice(line.indexOf('['))
538
+ if (image?.out && (filetype !== 'html' || siteRoot)) {
539
+ const attrlist = line.slice(line.indexOf('[') + 1, -1)
518
540
  pagesInOutline.assembled.assets.add(image)
519
- lines[idx] = `${prefix}image::${image.out.path}${boxedAttrlist}`
541
+ lines[idx] =
542
+ filetype === 'html'
543
+ ? `${prefix}image::${resolveLinkTarget(image, siteRoot, pubRoot, linkRefStyle, false)}[${attrlist}]`
544
+ : `${prefix}image::${resolveEmbedTarget(image, outDirname, embedRefStyle)}[${attrlist}]`
520
545
  }
521
546
  }
522
547
  lastImageMacroAt = [idx, imageMacroOffset]
@@ -555,6 +580,7 @@ function mergeAsciiDoc (
555
580
  }
556
581
  } else if (level) {
557
582
  if (atDocumentRoot && navtitlePlain === componentVersion.title) {
583
+ buffer.inBody ??= false
558
584
  level--
559
585
  } else {
560
586
  buffer.inBody = true
@@ -576,9 +602,18 @@ function mergeAsciiDoc (
576
602
  let sectionTitle = navtitleAsciiDoc
577
603
  if (urlType === 'external') {
578
604
  sectionTitle = `${url}[${navtitleAsciiDoc.replace(/\]/g, '\\]')}]`
579
- } else if (urlType === 'internal' && !unresolved && siteUrl) {
605
+ } else if (urlType === 'internal' && !unresolved) {
580
606
  const resource = files.find((it) => it.pub.url === url)
581
- if (resource) sectionTitle = `${siteUrl}${resource.pub.url}[${navtitleAsciiDoc.replace(/\]/g, '\\]')}]`
607
+ if (resource) {
608
+ if (resource.src.family === 'page' && pagesInOutline.has(resource.pub.url)) {
609
+ let refid = resource.src.relative.replace(/\.adoc$/, '').replace(/[/.]/g, '-')
610
+ if (refid.startsWith('./')) refid = topicPrefix + refid.slice(2)
611
+ if (resource.src.module !== 'ROOT') refid = `${resource.src.module}${idCoordinateSeparator}${refid}`
612
+ sectionTitle = `<<${refid},${navtitleAsciiDoc}>>`
613
+ } else if (siteRoot) {
614
+ sectionTitle = `${resolveLinkTarget(resource, siteRoot, pubRoot, linkRefStyle)}[${navtitleAsciiDoc.replace(/\]/g, '\\]')}]`
615
+ }
616
+ }
582
617
  }
583
618
  let hlevel = level + 1
584
619
  if (hlevel > 6) {
@@ -663,6 +698,12 @@ function processDocumentHeader (doc, lines, buffer, ignoreLines) {
663
698
  } else if (belowDoctitle) {
664
699
  if (implicitLines.length === 2 || !/[\p{Alpha}0-9]/u.test(chr0)) break
665
700
  implicitLines.push(line)
701
+ } else if (chr0 === '[' && line.charAt(line.length - 1) === ']') {
702
+ const attrlist = line
703
+ .slice(1, -1)
704
+ .trim()
705
+ .replace(/(?:^\w[\w-]*(?!=|[\w-]))?([#.%]\w[\w-]*)*/, '')
706
+ if (attrlist) buffer.push('[' + attrlist + ']')
666
707
  }
667
708
  } else if (belowDoctitle) {
668
709
  break
@@ -685,7 +726,7 @@ function generateSlug (title) {
685
726
  return String.fromCharCode(dec ? parseInt(dec, 10) : parseInt(hex, 16))
686
727
  })
687
728
  .replace(/[\x27\u2019]/g, '')
688
- .replace(/[^\p{Alpha}0-9\-]/gu, '-')
729
+ .replace(/[^\p{Alpha}0-9-]/gu, '-')
689
730
  .replace(/^-+|-+$|(-)-+/g, '$1')
690
731
  }
691
732
 
@@ -752,7 +793,19 @@ function isResourceSpec (str) {
752
793
  }
753
794
 
754
795
  function getObjectId (obj) {
755
- return global.Opal.uid()
796
+ return global.Opal.id(obj)
797
+ }
798
+
799
+ // NOTE: blank lines at top and bottom of document create mismatch when using line numbers to navigate source lines
800
+ // IMPORTANT: this must not leave behind lines the parser will drop!
801
+ // IDEA: another option is to capture initial lineno of reader and use as offset (but preseves those blank lines)
802
+ function trimAsciiDoc (buffer) {
803
+ return Buffer.from(
804
+ buffer
805
+ .toString()
806
+ .replace(/^(?:[ \t]*\r\n?|[ \t]*\n)+/, '')
807
+ .trimRight()
808
+ )
756
809
  }
757
810
 
758
811
  function safePush (onto, entries) {
@@ -765,4 +818,35 @@ function safePush (onto, entries) {
765
818
  }
766
819
  }
767
820
 
821
+ function resolveEmbedTarget (resource, outDirname, referenceStyle, escapeForInline) {
822
+ const target =
823
+ referenceStyle === 'output-relative' ? resource.out.path : path.relative(outDirname + '/', resource.out.path)
824
+ return escapeForInline ? target.replace(/_/g, '{underscore}') : target
825
+ }
826
+
827
+ function resolveLinkTarget (resource, siteRoot, pubRoot, referenceStyle, escapeForInline = true) {
828
+ let target
829
+ if (resource.site?.url) {
830
+ target = ['', resource.pub.url]
831
+ } else {
832
+ switch (referenceStyle) {
833
+ case 'absolute':
834
+ target = ['', siteRoot.url + resource.pub.url]
835
+ break
836
+ case 'root-relative':
837
+ target = ['link:', siteRoot.path + resource.pub.url]
838
+ break
839
+ default:
840
+ target = ['link:', computeRelativeUrl(pubRoot + '/', resource.pub.url)]
841
+ }
842
+ }
843
+ if (escapeForInline) target[1] = target[1].replace(/_/g, '{underscore}')
844
+ return target.join('')
845
+ }
846
+
847
+ function computeRelativeUrl (from, to) {
848
+ const rel = path.relative(from, to)
849
+ return to.charAt(to.length - 1) === '/' ? rel + '/' : rel
850
+ }
851
+
768
852
  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)
@@ -142,4 +177,39 @@ function prepareOutlines (navigation, rootEntry, rootLevel) {
142
177
  return items
143
178
  }
144
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
+
145
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'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antora/assembler",
3
- "version": "1.0.0-beta.5",
3
+ "version": "1.0.0-beta.7",
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)",