@antora/assembler 1.0.0-beta.1 → 1.0.0-beta.10

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,42 @@ 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 {
275
+ logger.info(ctx, line)
276
+ }
277
+ })
278
+ }
279
+ } else if (convertResult !== undefined) {
280
+ return convertResult
281
+ }
282
+ return fileOrContents === undefined
283
+ ? new LazyReadable(() => fs.createReadStream(convertAttributes.outfile))
284
+ : fileOrContents
285
+ }
286
+
223
287
  function isBound (obj) {
224
288
  if (obj == null) return false
225
- for (const p in obj) return true
289
+ for (const _ in obj) return true
226
290
  return false
227
291
  }
228
292
 
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,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
  }
@@ -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,52 @@ 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
+ const refid = generateId(resource.src, componentVersion, idCoordinateSeparator, idScopeSeparator, fragment).id
410
+ if (
411
+ text &&
412
+ (assemblyModel.dropExplicitXrefText === 'always' ||
413
+ (assemblyModel.dropExplicitXrefText === 'if-redundant' && text === resource.title))
414
+ ) {
415
+ text = ''
416
+ }
417
+ return `xref:${refid}[${text}]`
425
418
  })
426
419
  }
427
420
  if (~line.indexOf('link:{attachmentsdir}/')) {
428
421
  line = line.replace(/(?<![\\+])link:\{attachmentsdir\}\/([^\s[]+)\[(|.*?[^\\])\]/g, (m, relative, text) => {
429
422
  const attachment =
430
- siteUrl &&
423
+ siteRoot &&
431
424
  contentCatalog.getById({
432
425
  component: componentVersion.name,
433
426
  version: componentVersion.version,
@@ -436,7 +429,7 @@ function mergeAsciiDoc (
436
429
  relative,
437
430
  })
438
431
  // TODO: handle unresolved attachment page
439
- return attachment?.out ? `${siteUrl}${attachment.pub.url.replace(/_/g, '{underscore}')}[${text}]` : m
432
+ return attachment?.out ? `${resolveLinkTarget(attachment, siteRoot, pubRoot, linkRefStyle)}[${text}]` : m
440
433
  })
441
434
  }
442
435
  if (~line.indexOf('image:') && !line.startsWith('image::')) {
@@ -444,9 +437,11 @@ function mergeAsciiDoc (
444
437
  if (isResourceSpec(target)) {
445
438
  const image = contentCatalog.resolveResource(target, page.src, 'image', ['image'])
446
439
  // TODO: handle (or report) unresolved image better
447
- if (image?.out) {
440
+ if (image?.out && (filetype !== 'html' || siteRoot)) {
448
441
  pagesInOutline.assembled.assets.add(image)
449
- return `image:${image.out.path.replace(/_/g, '{underscore}')}[${attrlist}]`
442
+ return filetype === 'html'
443
+ ? `image:${resolveLinkTarget(image, siteRoot, pubRoot, linkRefStyle)}[${attrlist}]`
444
+ : `image:${resolveEmbedTarget(image, outDirname, embedRefStyle, true)}[${attrlist}]`
450
445
  }
451
446
  }
452
447
  return m
@@ -500,10 +495,13 @@ function mergeAsciiDoc (
500
495
  if (isResourceSpec(target)) {
501
496
  const image = contentCatalog.resolveResource(target, page.src, 'image', ['image'])
502
497
  // FIXME: handle (or report) case when image is not resolved
503
- if (image?.out) {
504
- const boxedAttrlist = line.slice(line.indexOf('['))
498
+ if (image?.out && (filetype !== 'html' || siteRoot)) {
499
+ const attrlist = line.slice(line.indexOf('[') + 1, -1)
505
500
  pagesInOutline.assembled.assets.add(image)
506
- lines[idx] = `${prefix}image::${image.out.path}${boxedAttrlist}`
501
+ lines[idx] =
502
+ filetype === 'html'
503
+ ? `${prefix}image::${resolveLinkTarget(image, siteRoot, pubRoot, linkRefStyle, false)}[${attrlist}]`
504
+ : `${prefix}image::${resolveEmbedTarget(image, outDirname, embedRefStyle)}[${attrlist}]`
507
505
  }
508
506
  }
509
507
  lastImageMacroAt = [idx, imageMacroOffset]
@@ -542,6 +540,7 @@ function mergeAsciiDoc (
542
540
  }
543
541
  } else if (level) {
544
542
  if (atDocumentRoot && navtitlePlain === componentVersion.title) {
543
+ buffer.inBody ??= false
545
544
  level--
546
545
  } else {
547
546
  buffer.inBody = true
@@ -563,9 +562,16 @@ function mergeAsciiDoc (
563
562
  let sectionTitle = navtitleAsciiDoc
564
563
  if (urlType === 'external') {
565
564
  sectionTitle = `${url}[${navtitleAsciiDoc.replace(/\]/g, '\\]')}]`
566
- } else if (urlType === 'internal' && !unresolved && siteUrl) {
565
+ } else if (urlType === 'internal' && !unresolved) {
567
566
  const resource = files.find((it) => it.pub.url === url)
568
- if (resource) sectionTitle = `${siteUrl}${resource.pub.url}[${navtitleAsciiDoc.replace(/\]/g, '\\]')}]`
567
+ if (resource) {
568
+ if (resource.src.family === 'page' && pagesInOutline.has(resource.pub.url)) {
569
+ const refid = generateId(resource.src, componentVersion, idCoordinateSeparator, idScopeSeparator).id
570
+ sectionTitle = `xref:${refid}[${navtitleAsciiDoc.replace(/\]/g, '\\]')}]`
571
+ } else if (siteRoot) {
572
+ sectionTitle = `${resolveLinkTarget(resource, siteRoot, pubRoot, linkRefStyle)}[${navtitleAsciiDoc.replace(/\]/g, '\\]')}]`
573
+ }
574
+ }
569
575
  }
570
576
  let hlevel = level + 1
571
577
  if (hlevel > 6) {
@@ -621,12 +627,8 @@ function processDocumentHeader (doc, lines, buffer, ignoreLines) {
621
627
  }
622
628
  const line = lines[idx]
623
629
  if (open === ':' || open === '-:') {
624
- if (line) {
625
- if (open === ':') buffer.push(line)
626
- if (!line.endsWith(' \\')) open = undefined
627
- } else {
628
- open = undefined
629
- }
630
+ if (open === ':') buffer.push(line)
631
+ if (!line || !line.endsWith(' \\')) open = undefined
630
632
  } else if (line) {
631
633
  const chr0 = line.charAt()
632
634
  let attributeEntryMatch
@@ -642,7 +644,7 @@ function processDocumentHeader (doc, lines, buffer, ignoreLines) {
642
644
  } else if (chr0 === ':' && ~line.indexOf(':', 2) && (attributeEntryMatch = line.match(AttributeEntryRx))) {
643
645
  const attributeName = attributeEntryMatch[1].replace('!', '')
644
646
  if (DiscardAttributes.includes(attributeName)) {
645
- if (line.endsWith(' \\')) open = '-:'
647
+ if (line.endsWith(' \\')) open = '-:' // disallow value continuation
646
648
  } else {
647
649
  if (line.endsWith(' \\')) open = ':'
648
650
  buffer.push(line)
@@ -650,6 +652,12 @@ function processDocumentHeader (doc, lines, buffer, ignoreLines) {
650
652
  } else if (belowDoctitle) {
651
653
  if (implicitLines.length === 2 || !/[\p{Alpha}0-9]/u.test(chr0)) break
652
654
  implicitLines.push(line)
655
+ } else if (chr0 === '[' && line.charAt(line.length - 1) === ']') {
656
+ const attrlist = line
657
+ .slice(1, -1)
658
+ .trim()
659
+ .replace(/(?:^\w[\w-]*(?!=|[\w-]))?([#.%]\w[\w-]*)*/, '')
660
+ if (attrlist) buffer.push('[' + attrlist + ']')
653
661
  }
654
662
  } else if (belowDoctitle) {
655
663
  break
@@ -672,7 +680,7 @@ function generateSlug (title) {
672
680
  return String.fromCharCode(dec ? parseInt(dec, 10) : parseInt(hex, 16))
673
681
  })
674
682
  .replace(/[\x27\u2019]/g, '')
675
- .replace(/[^\p{Alpha}0-9\-]/gu, '-')
683
+ .replace(/[^\p{Alpha}0-9-]/gu, '-')
676
684
  .replace(/^-+|-+$|(-)-+/g, '$1')
677
685
  }
678
686
 
@@ -739,7 +747,19 @@ function isResourceSpec (str) {
739
747
  }
740
748
 
741
749
  function getObjectId (obj) {
742
- return global.Opal.uid()
750
+ return global.Opal.id(obj)
751
+ }
752
+
753
+ // NOTE: blank lines at top and bottom of document create mismatch when using line numbers to navigate source lines
754
+ // IMPORTANT: this must not leave behind lines the parser will drop!
755
+ // IDEA: another option is to capture initial lineno of reader and use as offset (but preseves those blank lines)
756
+ function trimAsciiDoc (buffer) {
757
+ return Buffer.from(
758
+ buffer
759
+ .toString()
760
+ .replace(/^(?:[ \t]*\r\n?|[ \t]*\n)+/, '')
761
+ .trimRight()
762
+ )
743
763
  }
744
764
 
745
765
  function safePush (onto, entries) {
@@ -752,4 +772,54 @@ function safePush (onto, entries) {
752
772
  }
753
773
  }
754
774
 
775
+ function resolveEmbedTarget (resource, outDirname, referenceStyle, escapeForInline) {
776
+ const target =
777
+ referenceStyle === 'output-relative' ? resource.out.path : path.relative(outDirname + '/', resource.out.path)
778
+ return escapeForInline ? target.replace(/_/g, '{underscore}') : target
779
+ }
780
+
781
+ function resolveLinkTarget (resource, siteRoot, pubRoot, referenceStyle, escapeForInline = true) {
782
+ let target
783
+ if (resource.site?.url) {
784
+ target = ['', resource.pub.url]
785
+ } else {
786
+ switch (referenceStyle) {
787
+ case 'absolute':
788
+ target = ['', siteRoot.url + resource.pub.url]
789
+ break
790
+ case 'root-relative':
791
+ target = ['link:', siteRoot.path + resource.pub.url]
792
+ break
793
+ default:
794
+ target = ['link:', computeRelativeUrl(pubRoot + '/', resource.pub.url)]
795
+ }
796
+ }
797
+ if (escapeForInline) target[1] = target[1].replace(/_/g, '{underscore}')
798
+ return target.join('')
799
+ }
800
+
801
+ function computeRelativeUrl (from, to) {
802
+ const rel = path.relative(from, to)
803
+ return to.charAt(to.length - 1) === '/' ? rel + '/' : rel
804
+ }
805
+
806
+ function createResourceKey ({ component, version, module: mod, family, relative }) {
807
+ return `${version}@${component}:${mod === 'ROOT' ? '' : mod}:${family === 'page' ? '' : family + '$'}${relative}`
808
+ }
809
+
810
+ function generateId ({ component, module: mod, relative }, componentVersion, coordinateSep, scopeSep, fragment) {
811
+ let id = relative.replace(/\.adoc$/, '').replace(/[/.]/g, '-')
812
+ if (component !== componentVersion.name) {
813
+ id = [component, mod === 'ROOT' ? '' : mod, id].join(coordinateSep)
814
+ } else if (mod !== 'ROOT') {
815
+ id = mod + coordinateSep + id
816
+ } else if (ReservedIdNames.includes(id)) {
817
+ id += scopeSep
818
+ scopeSep = ''
819
+ }
820
+ const idPrefix = id + scopeSep
821
+ if (fragment) id = idPrefix + fragment
822
+ return { idPrefix, id }
823
+ }
824
+
755
825
  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.10",
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": [