@antora/assembler 1.0.0-beta.17 → 1.0.0-beta.19

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.
@@ -5,10 +5,12 @@ const fs = require('node:fs')
5
5
  const { promises: fsp } = fs
6
6
  const LazyReadable = require('./util/lazy-readable')
7
7
  const loadConfig = require('./load-config')
8
+ const logCommand = require('./log-command')
8
9
  const ospath = require('node:path')
9
10
  const { posix: path } = ospath
10
11
  const produceAssemblyFiles = require('./produce-assembly-files')
11
12
  const PromiseQueue = require('./util/promise-queue')
13
+ const runCommand = require('@antora/run-command-helper')
12
14
  const { stringify: toJSON } = JSON
13
15
 
14
16
  const invariably = { new: () => ({}), void: () => undefined }
@@ -16,17 +18,6 @@ const PACKAGE_NAME = require('../package.json').name
16
18
  const NEWLINE_RX = /(?:\r?\n)+/
17
19
 
18
20
  async function assembleContent (playbook, contentCatalog, converter, { configSource, navigationCatalog }) {
19
- const context = isBound(this)
20
- ? this
21
- : {
22
- getFunctions: invariably.new,
23
- getLogger: invariably.void,
24
- getVariables: invariably.new,
25
- }
26
- const assemblerConfig = await loadConfig.call(context, playbook, configSource)
27
- if (assemblerConfig.enabled === false) return []
28
- const generatorFunctions = context.getFunctions()
29
- const { loadAsciiDoc = require('@antora/asciidoc-loader') } = generatorFunctions
30
21
  const {
31
22
  convert = converter,
32
23
  getDefaultCommand,
@@ -36,13 +27,24 @@ async function assembleContent (playbook, contentCatalog, converter, { configSou
36
27
  mediaType: targetMediaType,
37
28
  loggerName = PACKAGE_NAME,
38
29
  } = converter ?? {}
30
+ const assemblerConfig = await loadConfig.call(this, playbook, configSource, '-' + targetBackend)
31
+ if (assemblerConfig.enabled === false) return []
32
+ const context = isBound(this)
33
+ ? this
34
+ : {
35
+ getFunctions: invariably.new,
36
+ getLogger: invariably.void,
37
+ getVariables: invariably.new,
38
+ }
39
+ const generatorFunctions = context.getFunctions()
40
+ const { loadAsciiDoc = require('@antora/asciidoc-loader') } = generatorFunctions
39
41
  const { assembly: assemblyConfig, build: buildConfig } = assemblerConfig
40
42
  assemblyConfig.embedReferenceStyle = embedReferenceStyle
41
43
  const { profile = targetBackend } = assemblyConfig
42
44
  const intrinsicAttributes = { 'loader-assembler': '' }
43
45
  buildConfig.cwd ??= process.cwd()
44
46
  if (profile) {
45
- buildConfig.dir ??= ospath.join(playbook.dir ?? process.cwd(), `build/assembler-${profile}`)
47
+ buildConfig.dir ??= ospath.join(playbook.dir ?? process.cwd(), `build/assembler/${profile}`)
46
48
  intrinsicAttributes[`assembler-profile-${profile}`] = ''
47
49
  intrinsicAttributes['assembler-profile'] = profile
48
50
  if (targetBackend) {
@@ -50,14 +52,14 @@ async function assembleContent (playbook, contentCatalog, converter, { configSou
50
52
  intrinsicAttributes['assembler-backend'] = targetBackend
51
53
  }
52
54
  } else {
53
- buildConfig.dir ??= ospath.join(playbook.dir ?? process.cwd(), 'build/assembler')
55
+ buildConfig.dir ??= ospath.join(playbook.dir ?? process.cwd(), 'build/assembler/_')
54
56
  }
55
57
  if (targetExtname) {
56
58
  const targetFiletype = targetExtname.slice(1)
57
59
  intrinsicAttributes[`assembler-filetype-${targetFiletype}`] = ''
58
60
  intrinsicAttributes['assembler-filetype'] = targetFiletype
59
61
  }
60
- Object.assign(assemblerConfig.assembly.attributes, intrinsicAttributes)
62
+ Object.assign(assemblyConfig.attributes, intrinsicAttributes)
61
63
  const assemblyFiles = produceAssemblyFiles(
62
64
  loadAsciiDoc,
63
65
  contentCatalog,
@@ -70,14 +72,15 @@ async function assembleContent (playbook, contentCatalog, converter, { configSou
70
72
  }
71
73
  const { publishSite: publishFiles = require('@antora/site-publisher') } = generatorFunctions
72
74
  await prepareWorkspace(publishFiles, assemblyFiles, buildConfig)
75
+ const helpers = { logCommand: logCommand.bind(context, loggerName), runCommand }
73
76
  const boundConvert = convert.bind(context)
74
77
  return new PromiseQueue({ concurrency: buildConfig.processLimit })
75
78
  .add(
76
79
  assemblyFiles.map((doc) => async () => {
77
80
  const relativeToOutput = embedReferenceStyle === 'output-relative'
78
- const convertAttributes = prepareConvertAttributes(doc, targetExtname, relativeToOutput, buildConfig)
81
+ const convertAttributes = prepareConvertAttributes(doc, targetExtname, relativeToOutput, assemblerConfig)
79
82
  if (buildConfig.mkdirs) await fsp.mkdir(convertAttributes.outdir, { recursive: true, force: true })
80
- return boundConvert(doc, convertAttributes, buildConfig).then((result) => {
83
+ return boundConvert(doc, convertAttributes, buildConfig, helpers).then((result) => {
81
84
  const fileOrContents = resolveFileOrContents.call(context, result, convertAttributes, buildConfig, loggerName)
82
85
  return coerceToExportFormat(doc, targetBackend, targetExtname, targetMediaType, fileOrContents)
83
86
  })
@@ -178,21 +181,21 @@ function generateSelectAssemblyProfile (context, contentCatalog, base, intrinsic
178
181
  }
179
182
  }
180
183
 
181
- function prepareConvertAttributes (doc, targetExtname, relativeToOutput, buildConfig) {
184
+ function prepareConvertAttributes (doc, targetExtname, relativeToOutput, assemblerConfig) {
182
185
  const {
183
186
  asciidoc: { attributes: docAttributes } = { attributes: {} },
184
187
  extname: docfilesuffix,
185
188
  path: reldocfile,
186
189
  src: { family, relative },
187
190
  } = doc
188
- const { cwd = process.cwd(), dir = cwd } = buildConfig
191
+ const { cwd = process.cwd(), dir = cwd } = assemblerConfig.build
189
192
  const docname = family + '$' + relative.slice(0, relative.length - docfilesuffix.length)
190
193
  const docfile = ospath.join(dir, reldocfile)
191
194
  const outdir = ospath.dirname(docfile)
192
195
  const docdir = relativeToOutput ? dir : outdir
193
196
  const imagesdir = ''
194
197
  const outfile = docfile.slice(0, docfile.length - docfilesuffix.length) + targetExtname
195
- const attributes = Object.assign({}, docAttributes, {
198
+ const attributes = Object.assign({ revdate: `${assemblerConfig.assembly.revdate}@` }, docAttributes, {
196
199
  docdir,
197
200
  docfile,
198
201
  docfilesuffix,
@@ -256,7 +259,13 @@ function prepareWorkspace (publishFiles, assemblyFiles, buildConfig) {
256
259
  outPaths.add(asset.out.path)
257
260
  }
258
261
  }
259
- if (keepSource) files.push(...assemblyFiles.map((file) => Object.assign(file, { out: { path: file.path } })))
262
+ if (keepSource) {
263
+ for (const file of assemblyFiles) {
264
+ file.src.contents = file.contents
265
+ file.out = { path: file.path }
266
+ files.push(file)
267
+ }
268
+ }
260
269
  return publishFiles({ output: { clean, dir } }, { getFiles: () => files })
261
270
  }
262
271
 
package/lib/configure.js CHANGED
@@ -10,21 +10,41 @@ function internalConfigure (converter, config = {}, providers = {}) {
10
10
  this.updateVariables({ assemblerProfiles: getAssemblerProfiles(contentCatalog) })
11
11
  })
12
12
 
13
- this.once('beforeProcess', ({ siteAsciiDocConfig }) => {
14
- siteAsciiDocConfig.keepSource = true
15
- })
13
+ this.once('beforeProcess', enableKeepSource)
16
14
 
17
15
  this.once('navigationBuilt', async ({ playbook, contentCatalog }) => {
18
16
  const { assembleContent = require('./assemble-content'), ...assembleContentConfig } = providers
19
- assembleContentConfig.configSource ??= config.configFile
20
- await assembleContent.call(this, playbook, contentCatalog, converter, assembleContentConfig)
17
+ if (assembleContentConfig.configSource?.constructor === Object) {
18
+ await assembleContent.call(this, playbook, contentCatalog, converter, assembleContentConfig)
19
+ } else {
20
+ const singleConfig = !('configFiles' in config)
21
+ const configFiles = singleConfig ? config.configFile : config.configFiles
22
+ for (const configSource of Array.isArray(configFiles) ? configFiles : [configFiles]) {
23
+ const assembleContentConfigWithConfigSource = Object.assign({}, assembleContentConfig, { configSource })
24
+ await assembleContent.call(this, playbook, contentCatalog, converter, assembleContentConfigWithConfigSource)
25
+ if (singleConfig) break
26
+ }
27
+ }
21
28
  })
22
29
  }
23
30
 
31
+ function enableKeepSource ({ siteAsciiDocConfig }) {
32
+ if (siteAsciiDocConfig.keepSource instanceof Boolean) return
33
+ siteAsciiDocConfig.keepSource = Object.assign(new Boolean(true), { oldValue: siteAsciiDocConfig.keepSource })
34
+ this.once('navigationBuilt', restoreKeepSource)
35
+ }
36
+
37
+ function restoreKeepSource ({ siteAsciiDocConfig, contentCatalog }) {
38
+ if (!(siteAsciiDocConfig.keepSource instanceof Boolean)) return
39
+ if ((siteAsciiDocConfig.keepSource = siteAsciiDocConfig.keepSource.oldValue)) return
40
+ contentCatalog.getPages((page) => delete page.src.contents)
41
+ }
42
+
24
43
  function getAssemblerProfiles (contentCatalog, assemblerProfiles = new Map()) {
25
44
  contentCatalog.getComponents().forEach((component) => {
26
45
  component.versions.forEach((componentVersion) => {
27
- const source = componentVersion.nav?.origin
46
+ const source =
47
+ componentVersion.nav?.origin ?? componentVersion.origins?.find((it) => it.descriptor?.ext?.assembler)
28
48
  const assemblerConfig = getAssemblerConfigFromDescriptor(source?.descriptor)
29
49
  if (!assemblerConfig) return
30
50
  const componentVersionRef = `${componentVersion.version}@${componentVersion.name}`
@@ -5,8 +5,6 @@ const fsp = require('node:fs/promises')
5
5
  const os = require('node:os')
6
6
  const yaml = require('js-yaml')
7
7
 
8
- const PACKAGE_NAME = require('../package.json').name
9
-
10
8
  const ASSEMBLY_KEYS = [
11
9
  'rootLevel',
12
10
  'insertStartPage',
@@ -15,18 +13,24 @@ const ASSEMBLY_KEYS = [
15
13
  'dropExplicitXrefText',
16
14
  ]
17
15
  const CAMEL_CASE_STOP_PATHS = ['asciidoc.attributes', 'assembly.attributes']
16
+ const PACKAGE_NAME = require('../package.json').name
18
17
 
19
- function loadConfig (playbook, configSource) {
18
+ function loadConfig (playbook, configSource, preferredQualifier = '') {
20
19
  let resolvedConfigSource
21
20
  return (
22
21
  configSource?.constructor === Object
23
22
  ? Promise.resolve(configSource)
24
- : fsp
25
- .access((resolvedConfigSource = expandPath(configSource ?? './antora-assembler.yml', { dot: playbook.dir })))
26
- .then(
27
- () => true,
28
- () => false
29
- )
23
+ : fileExists(
24
+ (resolvedConfigSource = expandPath(configSource ?? `./antora-assembler${preferredQualifier}.yml`, {
25
+ dot: playbook.dir,
26
+ }))
27
+ )
28
+ .then((exists) => {
29
+ if (exists || configSource || !preferredQualifier) return exists
30
+ return fileExists(
31
+ (resolvedConfigSource = resolvedConfigSource.slice(0, -(preferredQualifier.length + 4)) + '.yml')
32
+ )
33
+ })
30
34
  .then((exists) => {
31
35
  if (!exists) {
32
36
  let logger
@@ -134,6 +138,13 @@ function camelCaseKeys (o, stopPaths = [], p = undefined) {
134
138
  return accum
135
139
  }
136
140
 
141
+ function fileExists (path) {
142
+ return fsp.access(path).then(
143
+ () => true,
144
+ () => false
145
+ )
146
+ }
147
+
137
148
  function getLocalDate (now = new Date()) {
138
149
  return new Date(now - now.getTimezoneOffset() * 60000).toISOString().split('T')[0]
139
150
  }
@@ -0,0 +1,18 @@
1
+ 'use strict'
2
+
3
+ function logCommand (loggerName, command, file, convertAttributes, attributeOptionFlagOrArgs) {
4
+ const logger = this?.getLogger(loggerName)
5
+ if (!logger?.isLevelEnabled('debug')) return
6
+ const { docfile, 'assembler-filetype': filetype } = convertAttributes
7
+ const args = Array.isArray(attributeOptionFlagOrArgs)
8
+ ? attributeOptionFlagOrArgs
9
+ : attributeOptionFlagOrArgs
10
+ ? convertAttributes.toArgs(attributeOptionFlagOrArgs, command)
11
+ : []
12
+ const ctx = { command: [command].concat(args).join(' '), file: { path: docfile } }
13
+ const msg = `Running external command to export assembly in %s to %s: %s`
14
+ const componentVersionStr = file.src.version ? `${file.src.version}@${file.src.component}` : file.src.component
15
+ logger.debug(ctx, msg, componentVersionStr, filetype, file.src.relative)
16
+ }
17
+
18
+ module.exports = logCommand
@@ -2,16 +2,30 @@
2
2
 
3
3
  const createAsciiDocFile = require('./util/create-asciidoc-file')
4
4
  const createResourceKey = require('./util/create-resource-key')
5
- const generateId = require('./util/generate-id')
5
+ const generateScopedId = require('./util/generate-scoped-id')
6
6
  const { resolveLinkTarget } = require('./util/resolver')
7
- const { rewriteXrefs, rewriteImageRef, rewriteInlineImages, rewriteStyleAttribute } = require('./util/rewriter')
7
+ const {
8
+ rewriteXrefs,
9
+ rewriteImageAttr,
10
+ rewriteImageRef,
11
+ rewriteInlineImages,
12
+ rewriteStyleAttribute,
13
+ } = require('./util/rewriter')
8
14
  const sanitize = require('./util/sanitize')
9
15
  const unconvertInlineAsciiDoc = require('./util/unconvert-inline-asciidoc')
10
16
 
11
- const AttributeEntryRx = /^:([^:-][^:]*):(?: .*)?$/
12
- const BuiltInNamedEntities = { amp: '&', apos: "'", gt: '>', lt: '<', nbsp: ' ', quot: '"' }
13
- const CharRefRx = /&(?:([a-z][a-z]+\d{0,2})|#(?:(\d{2,6})|x([a-z\d]{2,5})));/g
14
- const DiscardAttributes = 'doctype leveloffset assembly-navtitle assembly-style underscore'.split(' ')
17
+ const ATTR_ENTRY_RX = /^:(!?[\p{Alpha}0-9_][^:]*):(?: |$)/u
18
+ const BUILT_IN_NAMED_ENTITIES = { amp: '&', apos: "'", gt: '>', lt: '<', nbsp: ' ', quot: '"' }
19
+ const CHAR_REF_RX = /&(?:([a-z][a-z]+\d{0,2})|#(?:(\d{2,6})|x([a-z\d]{2,5})));/g
20
+ const DISCARD_ATTRIBUTE_NAMES = [
21
+ 'doctype',
22
+ 'leveloffset',
23
+ 'assembly-header-attributes',
24
+ 'assembly-navtitle',
25
+ 'assembly-style',
26
+ 'underscore',
27
+ ]
28
+ const { NAMED_ID_ATTR_RX } = require('./util/rx')
15
29
 
16
30
  function produceAssemblyFile (
17
31
  loadAsciiDoc,
@@ -25,7 +39,30 @@ function produceAssemblyFile (
25
39
  ) {
26
40
  const pagesByUrl = files.reduce((map, it) => (it.src.family === 'page' ? map.set(it.pub.url, it) : map), new Map())
27
41
  if (outline.urlType === 'internal' && !pagesByUrl.get(outline.url) && !(outline.items || []).length) return
28
- const pagesInOutline = selectPagesInOutline(outline, pagesByUrl, componentVersion)
42
+ const pagesInOutline = selectPagesInOutline(outline, pagesByUrl)
43
+ const { rootLevel, xmlIds } = assemblyModel
44
+ const idSeparators = {
45
+ prefix:
46
+ 'assembler-idprefix' in asciidocConfig.attributes ? (asciidocConfig.attributes['assembler-idprefix'] ?? '') : '_',
47
+ scope: xmlIds ? '---' : ':::',
48
+ coordinate: xmlIds ? '----' : ':',
49
+ generateIdFromTitle: generateIdFromTitle.bind(
50
+ loadAsciiDoc(
51
+ { contents: Buffer.alloc(0), src: { family: 'page', relative: 'generate-id-from-title.adoc' } },
52
+ undefined,
53
+ asciidocConfig
54
+ )
55
+ ),
56
+ }
57
+ const { name: component, version } = componentVersion
58
+ asciidocConfig = prepareAsciiDocConfig(
59
+ contentCatalog,
60
+ { component, version },
61
+ pagesInOutline,
62
+ asciidocConfig,
63
+ assemblyModel,
64
+ idSeparators
65
+ )
29
66
  const buffer = mergeAsciiDoc(
30
67
  loadAsciiDoc,
31
68
  contentCatalog,
@@ -34,29 +71,51 @@ function produceAssemblyFile (
34
71
  outline,
35
72
  files,
36
73
  pagesInOutline,
74
+ idSeparators,
37
75
  asciidocConfig,
38
76
  mutableAttributes,
39
77
  assemblyModel
40
78
  )
41
- const rootLevel = assemblyModel.rootLevel
42
79
  const stem = rootLevel === 0 ? 'index' : generateSlug(buffer.navtitle)
43
- const downloadStem = [componentVersion.name, componentVersion.version, rootLevel === 0 ? '' : stem]
44
- .filter((it) => it)
45
- .join('-')
80
+ const downloadStem = [component, version, rootLevel === 0 ? '' : stem].filter((it) => it).join('-')
46
81
  return createAsciiDocFile(contentCatalog, {
47
82
  asciidoc: asciidocConfig,
48
83
  assembler: { assembled: pagesInOutline.assembled, downloadStem, rootLevel },
49
84
  contents: Buffer.from(buffer.join('\n') + '\n'),
50
- src: {
51
- component: componentVersion.name,
52
- version: componentVersion.version,
53
- module: 'ROOT',
54
- family: 'export',
55
- relative: stem + '.adoc',
56
- },
85
+ src: { component, version, module: 'ROOT', family: 'export', relative: stem + '.adoc' },
57
86
  })
58
87
  }
59
88
 
89
+ function prepareAsciiDocConfig (contentCatalog, ctx, pagesInOutline, asciidocConfig, assemblyModel, idSeparators) {
90
+ let attributesModified
91
+ const configShared = asciidocConfig.$shared
92
+ const sharedAttributes = asciidocConfig.attributes
93
+ if (!configShared) {
94
+ if (configShared == null) {
95
+ const assets = pagesInOutline.assembled.assets
96
+ for (const [name, val] of Object.entries(sharedAttributes)) {
97
+ if (!(typeof val === 'string' && ~val.indexOf(':'))) continue
98
+ let newVal
99
+ if (!name.endsWith('-image') || !(newVal = rewriteImageAttr(val, contentCatalog, assemblyModel, ctx, assets))) {
100
+ if (~(newVal = val).indexOf('image:')) {
101
+ newVal = rewriteInlineImages(newVal, contentCatalog, assemblyModel, ctx, assets)
102
+ }
103
+ }
104
+ if (newVal !== val) sharedAttributes[name] = newVal
105
+ }
106
+ }
107
+ for (const [name, val] of Object.entries(sharedAttributes)) {
108
+ if (!(typeof val === 'string' && ~val.indexOf(':'))) continue
109
+ if (~val.indexOf('xref:')) {
110
+ const newVal = rewriteXrefs(val, contentCatalog, assemblyModel, ctx, false, pagesInOutline, idSeparators)
111
+ if (newVal !== val) (attributesModified ??= {})[name] = newVal
112
+ }
113
+ }
114
+ if (configShared == null) Object.defineProperty(asciidocConfig, '$shared', { value: attributesModified == null })
115
+ }
116
+ return Object.assign({}, asciidocConfig, { attributes: Object.assign({}, sharedAttributes, attributesModified) })
117
+ }
118
+
60
119
  function buildAsciiDocHeader (componentVersion, navtitle, assemblyModel) {
61
120
  const doctype = assemblyModel.doctype ?? 'book'
62
121
  const navtitlePlain = sanitize(navtitle)
@@ -64,29 +123,30 @@ function buildAsciiDocHeader (componentVersion, navtitle, assemblyModel) {
64
123
  let doctitle = navtitleAsciiDoc
65
124
  if (navtitlePlain !== componentVersion.title) doctitle = `${componentVersion.title}: ${doctitle}`
66
125
  const version = componentVersion.version === 'master' ? '' : componentVersion.version
126
+ const displayVersion = componentVersion.displayVersion
67
127
  const buffer = [
68
128
  `= ${doctitle}`,
69
- ...(version ? [`:revnumber: ${version}`] : []),
129
+ ...(version ? [`:revnumber: ${displayVersion}`] : []),
70
130
  ...(doctype === 'article' ? [] : [`:doctype: ${doctype ?? 'book'}`]),
71
131
  ':underscore: _',
72
132
  // Q: should we pass these via the CLI so they cannot be modified?
73
133
  `:page-component-name: ${componentVersion.name}`,
74
134
  `:page-component-version:${version ? ' ' + version : ''}`,
75
135
  ':page-version: {page-component-version}',
76
- `:page-component-display-version: ${componentVersion.displayVersion}`,
136
+ `:page-component-display-version: ${displayVersion}`,
77
137
  `:page-component-title: ${componentVersion.title}`,
78
138
  ]
79
139
  return Object.assign(buffer, { navtitle })
80
140
  }
81
141
 
82
- function selectPagesInOutline (outlineEntry, pagesByUrl, componentVersion, accum) {
142
+ function selectPagesInOutline (outlineEntry, pagesByUrl, accum) {
83
143
  accum ??= Object.assign(new Map(), { assembled: { pages: new Map(), assets: new Set() } })
84
144
  const page = outlineEntry.urlType === 'internal' ? pagesByUrl.get(outlineEntry.url) : undefined
85
145
  if (page) {
86
146
  accum.set(createResourceKey(page.src), page)
87
147
  accum.set(outlineEntry.url, page)
88
148
  }
89
- for (const item of outlineEntry.items || []) selectPagesInOutline(item, pagesByUrl, componentVersion, accum)
149
+ for (const item of outlineEntry.items || []) selectPagesInOutline(item, pagesByUrl, accum)
90
150
  return accum
91
151
  }
92
152
 
@@ -98,6 +158,7 @@ function mergeAsciiDoc (
98
158
  outlineEntry,
99
159
  files,
100
160
  pagesInOutline,
161
+ idSeparators,
101
162
  asciidocConfig,
102
163
  mutableAttributes,
103
164
  assemblyModel,
@@ -114,18 +175,14 @@ function mergeAsciiDoc (
114
175
  let navtitlePlain = sanitize(navtitle)
115
176
  let navtitleAsciiDoc = unconvertInlineAsciiDoc(navtitle)
116
177
  const { items = [], unresolved, urlType, url } = outlineEntry
117
- const { doctype, linkReferenceStyle, pubRoot, siteRoot, xmlIds, logger } = assemblyModel
178
+ const { doctype, filetype, linkReferenceStyle, pubRoot, siteRoot, logger } = assemblyModel
118
179
  const assembled = pagesInOutline.assembled
119
180
  // FIXME: ideally, resource ID would be stored in navigation so we can look up the page more efficiently
120
181
  const page = urlType === 'internal' && !unresolved ? pagesInOutline.get(url) : undefined
121
182
  const atDocumentRoot = !buffer.inBody
183
+ const isRootPage = page && atDocumentRoot && !level
122
184
  const atBookRoot = atDocumentRoot && !level && doctype === 'book' && (supportsParts = true)
123
185
  const hasItems = items.length > 0
124
- const idPrefix =
125
- asciidocConfig.attributes['assembler-idprefix'] ?? ('assembler-idprefix' in asciidocConfig.attributes ? '' : '_')
126
- const idSeparator = xmlIds ? '-' : ':'
127
- const idScopeSeparator = idSeparator.repeat(3)
128
- const idCoordinateSeparator = idSeparator === '-' ? '----' : idSeparator
129
186
  if (page && !assembled.pages.has(page)) {
130
187
  const contents = page.src.contents
131
188
  if (contents == null) {
@@ -146,6 +203,7 @@ function mergeAsciiDoc (
146
203
  },
147
204
  })
148
205
  : doc.getLogger()
206
+ doc.source_header_attributes ??= doc.parent.$to_h()
149
207
  if (doc.hasAttribute('assembly-navtitle')) {
150
208
  navtitleAsciiDoc = doc.getAttribute('assembly-navtitle')
151
209
  navtitlePlain = sanitize((navtitle = doc.$apply_reftext_subs(navtitleAsciiDoc)))
@@ -159,30 +217,21 @@ function mergeAsciiDoc (
159
217
  }
160
218
  }
161
219
  }
162
- if (atDocumentRoot) {
163
- const authors = doc.getAuthors()
164
- if (authors.length) {
165
- const authorLine = authors
166
- .map((author) => {
167
- const email = author.getEmail()
168
- return email ? `${author.getName()} <${author.getEmail()}>` : author.getName()
169
- })
170
- .join('; ')
171
- buffer.splice(1, 0, authorLine)
172
- }
173
- }
174
220
  // NOTE: in Antora, docname is relative src path from module without file extension
175
221
  const docname = doc.getAttribute('docname')
176
- const { idLeader, id: idScope } = generateId(
177
- page.src,
178
- componentVersion,
179
- idCoordinateSeparator,
180
- idScopeSeparator,
181
- idPrefix
182
- )
222
+ const pageId = generateScopedId(page.src, componentVersion, idSeparators, filetype)
223
+ const pageIdLeader = pageId + idSeparators.scope
183
224
  let pageFragment = ''
184
225
  let pageRoles = ''
185
226
  let pageStyle = doc.getAttribute('assembly-style', '')
227
+ if (
228
+ !pageStyle &&
229
+ atBookRoot &&
230
+ doc.isAttribute('preface-title') &&
231
+ !('assembly-style' in doc.source_header_attributes.$$smap)
232
+ ) {
233
+ pageStyle = 'preface'
234
+ }
186
235
  let part
187
236
  if ((part = pageStyle === 'part')) {
188
237
  pageStyle = ''
@@ -194,7 +243,10 @@ function mergeAsciiDoc (
194
243
  let nextSectionLevel = 1
195
244
  const lines = doc.getSourceLines()
196
245
  const ignoreLines = []
197
- buffer.inBody = true
246
+ if (!buffer.inBody) {
247
+ buffer.inBody = true
248
+ buffer.endHeaderIdx = buffer.length - 1
249
+ }
198
250
  buffer.push('')
199
251
  buffer.push(`:docname: ${docname}`)
200
252
  if (component !== lastComponentVersion.name) {
@@ -218,7 +270,63 @@ function mergeAsciiDoc (
218
270
  buffer.push(`:page-origin-refname: ${origin.branch || origin.tag}`)
219
271
  buffer.push(`:page-origin-reftype: ${origin.branch ? 'branch' : 'tag'}`)
220
272
  buffer.push(`:page-origin-refhash: ${origin.worktree ? '(worktree)' : origin.refhash}`)
221
- if (doc.hasHeader()) pageRoles = processDocumentHeader(doc, lines, buffer, ignoreLines)
273
+ if (doc.hasHeader()) {
274
+ for (const entry of processDocumentHeader(doc, lines, ignoreLines, isRootPage)) {
275
+ if (entry.type === 'author_line') {
276
+ buffer.splice(1, 0, entry.lines[0])
277
+ buffer.endHeaderIdx += 1
278
+ } else if (entry.type === 'attribute_entry') {
279
+ const name = entry.name
280
+ if (!entry.negated && !doc.isAttributeLocked(name)) {
281
+ let val, newVal
282
+ if (
283
+ name.endsWith('-image') &&
284
+ (val = doc.getAttribute(name)) &&
285
+ (newVal = rewriteImageAttr(val, contentCatalog, assemblyModel, page.src, assembled.assets))
286
+ ) {
287
+ if (newVal !== val) entry.lines = [`:${name}: ${newVal}`]
288
+ } else if (~(val = entry.lines.join('\n').slice(name.length + 3)).indexOf(':')) {
289
+ newVal = val
290
+ if (~newVal.indexOf('image:')) {
291
+ newVal = rewriteInlineImages(
292
+ newVal,
293
+ contentCatalog,
294
+ assemblyModel,
295
+ page.src,
296
+ assembled.assets,
297
+ false,
298
+ doc
299
+ )
300
+ }
301
+ if (~newVal.indexOf('xref:')) {
302
+ newVal = rewriteXrefs(
303
+ newVal,
304
+ contentCatalog,
305
+ assemblyModel,
306
+ page.src,
307
+ false,
308
+ pagesInOutline,
309
+ idSeparators,
310
+ pageIdLeader,
311
+ doc,
312
+ { file: relative, lineno: entry.lineno }
313
+ )
314
+ }
315
+ if (newVal !== val) entry.lines = `:${entry.name}: ${newVal}`.split('\n')
316
+ }
317
+ }
318
+ if (entry.promote) {
319
+ buffer.splice(buffer.endHeaderIdx + 1, 0, ...entry.lines)
320
+ buffer.endHeaderIdx += entry.lines.length
321
+ } else {
322
+ buffer.push(...entry.lines)
323
+ }
324
+ } else {
325
+ buffer.push(...entry.lines)
326
+ }
327
+ }
328
+ pageRoles = doc.getRoles().reduce((accum, role) => `${accum}.${role}`, '')
329
+ }
222
330
  let heading
223
331
  if (pageStyle && (part && level === 1 ? (level = 0) : level) === 0) {
224
332
  let htitleAsciiDoc = navtitleAsciiDoc
@@ -235,10 +343,10 @@ function mergeAsciiDoc (
235
343
  assemblyModel = Object.assign({}, assemblyModel, { sectionMergeStrategy: 'discrete' })
236
344
  } else {
237
345
  if (atDocumentRoot) {
238
- pageFragment = `#${idLeader}${doc.getId() ?? pageStyle}`
239
- buffer.unshift(`[#${idScope}]`)
346
+ pageFragment = `#${pageIdLeader}${doc.getId() ?? pageStyle}`
347
+ buffer.unshift(`[#${pageId}]`)
240
348
  } else {
241
- pageFragment = `#${idScope}`
349
+ pageFragment = `#${pageId}`
242
350
  }
243
351
  heading = { title: htitleAsciiDoc, level: part ? 1 : 2 }
244
352
  nextSectionLevel++
@@ -247,7 +355,7 @@ function mergeAsciiDoc (
247
355
  if (atDocumentRoot && navtitlePlain === componentVersion.title) {
248
356
  level--
249
357
  } else {
250
- pageFragment = `#${idScope}`
358
+ pageFragment = `#${pageId}`
251
359
  if (part && level === 1) level--
252
360
  if ((heading = { title: navtitleAsciiDoc, level: level + 1 }).level > 6) {
253
361
  Object.assign(heading, { level: 6, style: `discrete.h${heading.level}` })
@@ -258,7 +366,7 @@ function mergeAsciiDoc (
258
366
  buffer.push(`[${heading.style ?? pageStyle}${pageFragment}${pageRoles}]`)
259
367
  buffer.push(`${'='.repeat(heading.level)} ${heading.title}`)
260
368
  } else if (atDocumentRoot) {
261
- buffer.unshift(`[#${idScope}]`)
369
+ buffer.unshift(`[#${pageId}]`)
262
370
  }
263
371
  let enclosed
264
372
  if (assemblyModel.sectionMergeStrategy === 'enclose' && hasItems && doc.hasSections()) {
@@ -268,26 +376,16 @@ function mergeAsciiDoc (
268
376
  //if (overviewTitle === navtitle) overviewTitle = doc.getAttribute('overview-title', 'Overview')
269
377
  const overviewTitle = doc.getAttribute('overview-title', 'Overview')
270
378
  buffer.push('')
271
- // NOTE: try to toggle sectids; otherwise, fallback to globally unique synthetic ID
272
- let toggleSectids, syntheticId
273
- if (doc.isAttribute('sectids')) {
274
- if (doc.isAttributeLocked('sectids')) {
275
- syntheticId = `__object-id-${getObjectId(outlineEntry)}`
276
- } else {
277
- buffer.push(':!sectids:')
278
- toggleSectids = true
279
- }
280
- }
379
+ const syntheticId = idSeparators.generateIdFromTitle(navtitleAsciiDoc, idSeparators)
281
380
  let hlevel = level + 2
282
381
  if (hlevel > 6) {
283
382
  const blockStyle = `discrete.h${hlevel}`
284
383
  hlevel = 6
285
- buffer.push(syntheticId ? `[${blockStyle}#${syntheticId}]` : `[${blockStyle}]`)
286
- } else if (syntheticId) {
384
+ buffer.push(`[${blockStyle}#${syntheticId}]`)
385
+ } else {
287
386
  buffer.push(`[#${syntheticId}]`)
288
387
  }
289
388
  buffer.push(`${'='.repeat(hlevel)} ${overviewTitle}`)
290
- if (toggleSectids) buffer.push(':sectids:')
291
389
  }
292
390
  assembled.pages.set(page, pageFragment)
293
391
  if (doc.hasSections()) {
@@ -363,7 +461,7 @@ function mergeAsciiDoc (
363
461
  if (
364
462
  line.charAt() === ':' &&
365
463
  ~line.indexOf(':', 2) &&
366
- (line.match(AttributeEntryRx) || ['', ''])[1].replace('!', '') === 'leveloffset'
464
+ (line.match(ATTR_ENTRY_RX) || ['', ''])[1].replace('!', '') === 'leveloffset'
367
465
  ) {
368
466
  if (lines[idx - 1] === '') lines[idx - 1] = undefined
369
467
  lines[idx] = undefined
@@ -375,13 +473,13 @@ function mergeAsciiDoc (
375
473
  if (!refs['$key?'](refid) && (~refid.indexOf(' ') || refid.toLowerCase() !== refid)) {
376
474
  if ((refid = doc.$resolve_id(refid))['$nil?']()) return m
377
475
  }
378
- return `<<${idLeader}${refid}${text ? ',' + text : ''}>>`
476
+ return `<<${pageIdLeader}${refid}${text ? ',' + text : ''}>>`
379
477
  })
380
478
  }
381
479
  // NOTE: the next check takes care of inline and block anchors
382
480
  if (~line.indexOf('[[')) {
383
481
  line = line.replace(/\[\[([\p{Alpha}_:][\p{Alpha}0-9_\-:.]*)(|, *.+?)\]\]/gu, (_, refid, text) => {
384
- return `[[${idLeader}${refid}${text}]]`
482
+ return `[[${pageIdLeader}${refid}${text}]]`
385
483
  })
386
484
  }
387
485
  if (~line.indexOf('xref:')) {
@@ -391,17 +489,15 @@ function mergeAsciiDoc (
391
489
  assemblyModel,
392
490
  page.src,
393
491
  true,
394
- idCoordinateSeparator,
395
- idScopeSeparator,
396
- idPrefix,
397
- idLeader,
398
492
  pagesInOutline,
493
+ idSeparators,
494
+ pageIdLeader,
399
495
  doc,
400
496
  { file: relative, lineno: idx + 1 }
401
497
  )
402
498
  }
403
499
  if (~line.indexOf('image:') && !line.startsWith('image::')) {
404
- line = rewriteInlineImages(line, contentCatalog, assemblyModel, page.src, assembled.assets, true)
500
+ line = rewriteInlineImages(line, contentCatalog, assemblyModel, page.src, assembled.assets, true, doc)
405
501
  }
406
502
  lines[idx] = line
407
503
  }
@@ -427,7 +523,7 @@ function mergeAsciiDoc (
427
523
  return '='.repeat(targetMarkerLength) + ' ' + rest
428
524
  })
429
525
  // NOTE: ID will be undefined if sectids are turned off
430
- if (block.getId()) rewriteStyleAttribute(block, lines, idx, idLeader, blockStyle)
526
+ if (block.getId()) rewriteStyleAttribute(block, lines, idx, pageIdLeader, blockStyle)
431
527
  } else {
432
528
  if (context === 'image') {
433
529
  let line = lines[idx] || ''
@@ -456,14 +552,14 @@ function mergeAsciiDoc (
456
552
  // nested document
457
553
  idx = (block.getHeader().getLineNumber() || idx + 1) - 1
458
554
  }
459
- if (block.getId()) rewriteStyleAttribute(block, lines, idx, idLeader)
555
+ if (block.getId()) rewriteStyleAttribute(block, lines, idx, pageIdLeader)
460
556
  }
461
557
  })
462
558
  safePush(
463
559
  buffer,
464
560
  lines.filter((it) => it !== undefined)
465
561
  )
466
- const attributeEntries = Object.entries(doc.source_header_attributes?.$$smap || {})
562
+ const attributeEntries = Object.entries(doc.source_header_attributes.$$smap)
467
563
  if (attributeEntries.length) {
468
564
  const resolvedAttributeEntries = attributeEntries.reduce(
469
565
  (accum, [name, val]) => {
@@ -475,7 +571,7 @@ function mergeAsciiDoc (
475
571
  } else if (val !== initialVal) {
476
572
  accum.push(`:${name}:${initialVal ? ' ' + initialVal : ''}`)
477
573
  }
478
- } else if (!(val == null || doc.isAttributeLocked(name) || DiscardAttributes.includes(name))) {
574
+ } else if (!(val == null || doc.isAttributeLocked(name) || DISCARD_ATTRIBUTE_NAMES.includes(name))) {
479
575
  accum.push(`:!${name}:`)
480
576
  }
481
577
  return accum
@@ -491,20 +587,7 @@ function mergeAsciiDoc (
491
587
  } else {
492
588
  buffer.inBody = true
493
589
  buffer.push('')
494
- // NOTE: try to toggle sectids; otherwise, fallback to globally unique synthetic ID
495
- // Q: should we unset docname, page-module, etc?
496
- let toggleSectids, syntheticId
497
- if (!('sectids' in asciidocConfig.attributes)) {
498
- buffer.push(':!sectids:')
499
- toggleSectids = true
500
- } else if (typeof asciidocConfig.attributes.sectids === 'string') {
501
- if ('sectids' in mutableAttributes) {
502
- buffer.push(':!sectids:')
503
- toggleSectids = true
504
- } else {
505
- syntheticId = `__object-id-${global.Opal.hash(outlineEntry).$object_id()}`
506
- }
507
- }
590
+ const syntheticId = idSeparators.generateIdFromTitle(navtitleAsciiDoc, idSeparators)
508
591
  let sectionTitle = navtitleAsciiDoc
509
592
  if (urlType === 'external') {
510
593
  sectionTitle = `${url}[${navtitleAsciiDoc.replace(/\]/g, '\\]')}]`
@@ -512,14 +595,7 @@ function mergeAsciiDoc (
512
595
  const resource = files.find((it) => it.pub.url === url)
513
596
  if (resource) {
514
597
  if (resource.src.family === 'page' && pagesInOutline.has(resource.pub.url)) {
515
- const refid = generateId(
516
- resource.src,
517
- componentVersion,
518
- idCoordinateSeparator,
519
- idScopeSeparator,
520
- idPrefix,
521
- true
522
- ).id
598
+ const refid = generateScopedId(resource.src, componentVersion, idSeparators, filetype, true)
523
599
  sectionTitle = `xref:${refid}[${navtitleAsciiDoc.replace(/\]/g, '\\]')}]`
524
600
  } else if (siteRoot) {
525
601
  sectionTitle = `${resolveLinkTarget(resource, siteRoot, pubRoot, linkReferenceStyle, true)}[${navtitleAsciiDoc.replace(/\]/g, '\\]')}]`
@@ -529,15 +605,13 @@ function mergeAsciiDoc (
529
605
  let hlevel = level + 1
530
606
  if (hlevel > 6) {
531
607
  hlevel = 6
532
- buffer.push(syntheticId ? `[discrete#${syntheticId}]` : '[discrete]')
533
- } else if (syntheticId) {
608
+ buffer.push(`[discrete#${syntheticId}]`)
609
+ } else {
534
610
  buffer.push(`[#${syntheticId}]`)
535
611
  }
536
612
  buffer.push(`${'='.repeat(hlevel)} ${sectionTitle}`)
537
- if (toggleSectids) buffer.push(':sectids:')
538
613
  }
539
614
  }
540
-
541
615
  if (hasItems) {
542
616
  const nextLevel = level + 1
543
617
  // NOTE: drop first child if same as parent; should we keep if content is different?
@@ -553,6 +627,7 @@ function mergeAsciiDoc (
553
627
  item,
554
628
  files,
555
629
  pagesInOutline,
630
+ idSeparators,
556
631
  asciidocConfig,
557
632
  mutableAttributes,
558
633
  assemblyModel,
@@ -565,11 +640,32 @@ function mergeAsciiDoc (
565
640
  return buffer
566
641
  }
567
642
 
568
- function processDocumentHeader (doc, lines, buffer, ignoreLines) {
643
+ function processDocumentHeader (doc, lines, ignoreLines, isRootPage) {
644
+ const entries = []
645
+ let assemblyHeaderAttributes = isRootPage && doc.getAttribute('assembly-header-attributes', '%authors')
646
+ assemblyHeaderAttributes = new Set(assemblyHeaderAttributes ? assemblyHeaderAttributes.split(/, */) : undefined)
647
+ if (assemblyHeaderAttributes.size) {
648
+ if (assemblyHeaderAttributes.has('%authors')) {
649
+ const authors = doc.getAuthors()
650
+ if (authors.length) {
651
+ entries.push({
652
+ type: 'author_line',
653
+ promote: true,
654
+ lines: [
655
+ authors
656
+ .map((author) => (author.getEmail() ? `${author.getName()} <${author.getEmail()}>` : author.getName()))
657
+ .join('; '),
658
+ ],
659
+ })
660
+ }
661
+ assemblyHeaderAttributes.delete('%authors')
662
+ }
663
+ const headerAttributes = doc.source_header_attributes
664
+ for (const name of assemblyHeaderAttributes) headerAttributes.$delete(name)
665
+ }
569
666
  const doctitleIdx = doc.getHeader().getLineNumber() - 1
570
667
  const end = doc.getBlocks()[0]?.getLineNumber() ?? lines.length
571
- let belowDoctitle
572
- let open
668
+ let belowDoctitle, current, open
573
669
  const implicitLines = []
574
670
  for (let idx = 0; idx < end; idx++) {
575
671
  if (idx === doctitleIdx) {
@@ -580,37 +676,51 @@ function processDocumentHeader (doc, lines, buffer, ignoreLines) {
580
676
  }
581
677
  const line = lines[idx]
582
678
  if (open === ':' || open === '-:') {
583
- if (open === ':') buffer.push(line)
584
- if (!line || !line.endsWith(' \\')) open = undefined
679
+ if (open === ':') current.lines.push(line)
680
+ if (!line || !line.endsWith(' \\')) current = open = undefined
585
681
  } else if (line) {
586
- const chr0 = line.charAt()
587
682
  let attributeEntryMatch
683
+ const chr0 = line.charAt()
588
684
  if (chr0 === '/' && line.charAt(1) === '/') {
589
- if (line.startsWith('////')) {
590
- open = open ? (open === line ? undefined : open) : line
591
- } else if (belowDoctitle && !open && line.charAt(2) === '/') {
685
+ if (line.startsWith('////') && line === '/'.repeat(line.length)) {
686
+ if (open) {
687
+ current.lines.push(line)
688
+ if (open === line) current = open = undefined
689
+ } else {
690
+ entries.push((current = { type: 'block_comment', lines: [(open = line)] }))
691
+ }
692
+ } else if (open) {
693
+ current.lines.push(line)
694
+ } else if (belowDoctitle && line.charAt(2) === '/') {
592
695
  break
696
+ } else {
697
+ entries.push({ type: 'line_comment', lines: [line] })
698
+ current = undefined
593
699
  }
594
- buffer.push(line)
595
700
  } else if (open) {
596
- buffer.push(line)
597
- } else if (chr0 === ':' && ~line.indexOf(':', 2) && (attributeEntryMatch = line.match(AttributeEntryRx))) {
598
- const attributeName = attributeEntryMatch[1].replace('!', '')
599
- if (DiscardAttributes.includes(attributeName)) {
701
+ current.lines.push(line)
702
+ } else if (chr0 === ':' && ~line.indexOf(':', 2) && (attributeEntryMatch = line.match(ATTR_ENTRY_RX))) {
703
+ let name = attributeEntryMatch[1]
704
+ const negated = name.charAt() === '!' || name.charAt(name.length - 1) === '!'
705
+ if (negated) name = name.replace('!', '')
706
+ if (DISCARD_ATTRIBUTE_NAMES.includes(name)) {
600
707
  if (line.endsWith(' \\')) open = '-:' // disallow value continuation
601
708
  } else {
602
709
  if (line.endsWith(' \\')) open = ':'
603
- buffer.push(line)
710
+ entries.push((current = { type: 'attribute_entry', name, negated, lines: [line], lineno: idx + 1 }))
711
+ if (assemblyHeaderAttributes.has(name)) current.promote = true
604
712
  }
605
713
  } else if (belowDoctitle) {
606
714
  if (implicitLines.length === 2 || !/[\p{Alpha}0-9]/u.test(chr0)) break
607
715
  implicitLines.push(line)
608
716
  } else if (chr0 === '[' && line.charAt(line.length - 1) === ']') {
717
+ current = undefined
609
718
  const attrlist = line
610
719
  .slice(1, -1)
611
720
  .trim()
612
- .replace(/(?:^\w[\w-]*(?!=|[\w-]))?([#.%]\w[\w-]*)*/, '')
613
- if (attrlist) buffer.push('[' + attrlist + ']')
721
+ .replace(/(?:^\w[\w-]*(?!=|[\w-]))?([#.]\w[\w-]*)*/, '')
722
+ .replace(NAMED_ID_ATTR_RX, '')
723
+ if (attrlist) entries.push({ type: 'attrlist', lines: ['[' + attrlist + ']'] })
614
724
  }
615
725
  } else if (belowDoctitle) {
616
726
  break
@@ -618,18 +728,15 @@ function processDocumentHeader (doc, lines, buffer, ignoreLines) {
618
728
  lines[idx] = undefined
619
729
  ignoreLines.push(idx)
620
730
  }
621
- return doc
622
- .getRoles()
623
- .map((role) => '.' + role)
624
- .join('')
731
+ return entries
625
732
  }
626
733
 
627
734
  function generateSlug (title) {
628
735
  return title
629
736
  .toLowerCase()
630
737
  .replace(/<[^>]+>/g, '')
631
- .replace(CharRefRx, (_, name, dec, hex) => {
632
- if (name) return BuiltInNamedEntities[name] ?? '?'
738
+ .replace(CHAR_REF_RX, (_, name, dec, hex) => {
739
+ if (name) return BUILT_IN_NAMED_ENTITIES[name] ?? '?'
633
740
  return String.fromCharCode(dec ? parseInt(dec, 10) : parseInt(hex, 16))
634
741
  })
635
742
  .replace(/[\x27\u2019]/g, '')
@@ -645,10 +752,6 @@ function fixSectionLevels (sections, expectedLevel) {
645
752
  })
646
753
  }
647
754
 
648
- function getObjectId (obj) {
649
- return global.Opal.id(obj)
650
- }
651
-
652
755
  // NOTE: blank lines at top and bottom of document create mismatch when using line numbers to navigate source lines
653
756
  // IMPORTANT: this must not leave behind lines the parser will drop!
654
757
  // IDEA: another option is to capture initial lineno of reader and use as offset (but preseves those blank lines)
@@ -671,4 +774,11 @@ function safePush (onto, entries) {
671
774
  }
672
775
  }
673
776
 
777
+ function generateIdFromTitle (titleAsciiDoc, idSeparators) {
778
+ const Section = this.$class().$const_get('::Asciidoctor::Section')
779
+ const baseId = Section.$generate_id(titleAsciiDoc, this)
780
+ this.getCatalog().refs['$[]='](baseId, true)
781
+ return `_${idSeparators.coordinate}${baseId}`
782
+ }
783
+
674
784
  module.exports = produceAssemblyFile
@@ -4,7 +4,6 @@ const computeOut = require('./util/compute-out')
4
4
  const createAsciiDocFile = require('./util/create-asciidoc-file')
5
5
  const filterComponentVersions = require('./filter-component-versions')
6
6
  const produceAssemblyFile = require('./produce-assembly-file')
7
- const { rewriteImageAttr, rewriteInlineImages, rewriteXrefs } = require('./util/rewriter')
8
7
  const selectMutableAttributes = require('./select-mutable-attributes')
9
8
 
10
9
  const ATTR_REF_RX = /\\?\{(\w[\w-]*)\}/g
@@ -43,7 +42,7 @@ function produceAssemblyFiles (loadAsciiDoc, contentCatalog, assemblerConfig, se
43
42
  delete assemblyAttributes['source-highlighter']
44
43
  }
45
44
  const mergedAsciiDocAttributes = collateAsciiDocAttributes(
46
- Object.assign({ revdate: assemblyModel.revdate }, componentVersionAsciiDocConfig.attributes),
45
+ Object.assign({}, componentVersionAsciiDocConfig.attributes),
47
46
  assemblyAttributes,
48
47
  contextualLogger
49
48
  )
@@ -75,22 +74,6 @@ function produceAssemblyFiles (loadAsciiDoc, contentCatalog, assemblerConfig, se
75
74
  } else if (!(assemblyModel.filetype === 'pdf' && assemblyModel.linkReferenceStyle === 'relative')) {
76
75
  assemblyModel.linkReferenceStyle = 'absolute'
77
76
  }
78
- const auxiliaryImages = new Set()
79
- const ctx = { component: componentName, version }
80
- Object.entries(mergedAsciiDocAttributes).forEach(([name, val]) => {
81
- if (!(typeof val === 'string' && ~val.indexOf(':'))) return
82
- if (name.endsWith('-image')) {
83
- const newVal = rewriteImageAttr(val, contentCatalog, assemblyModel, ctx, auxiliaryImages)
84
- if (newVal) {
85
- mergedAsciiDocAttributes[name] = newVal
86
- return
87
- }
88
- }
89
- const oldVal = val
90
- if (~val.indexOf('image:')) val = rewriteInlineImages(val, contentCatalog, assemblyModel, ctx, auxiliaryImages)
91
- if (~val.indexOf('xref:')) val = rewriteXrefs(val, contentCatalog, assemblyModel, ctx)
92
- if (val !== oldVal) mergedAsciiDocAttributes[name] = val
93
- })
94
77
  const rootEntry = { content: title }
95
78
  let startPage =
96
79
  'startPage' in componentVersion
@@ -115,7 +98,6 @@ function produceAssemblyFiles (loadAsciiDoc, contentCatalog, assemblerConfig, se
115
98
  })
116
99
  }
117
100
  const mutableAttributes = selectMutableAttributes(loadAsciiDoc, contentCatalog, startPage, mergedAsciiDocConfig)
118
- delete mutableAttributes.doctype // Q: should this be in selectMutableAttributes?
119
101
  prepareOutlines(navigation, rootEntry, rootLevel).reduce((any, outline) => {
120
102
  const assemblyFile = produceAssemblyFile(
121
103
  loadAsciiDoc,
@@ -128,19 +110,14 @@ function produceAssemblyFiles (loadAsciiDoc, contentCatalog, assemblerConfig, se
128
110
  assemblyModel
129
111
  )
130
112
  if (!assemblyFile) return any
131
- if (!any && auxiliaryImages.size) {
132
- const assembledAssets = assemblyFile.assembler.assembled.assets
133
- auxiliaryImages.forEach((asset) => assembledAssets.add(asset))
113
+ // NOTE restore source highlighter for conversion if defined in Assembler config
114
+ if (sourceHighlighter !== undefined) {
115
+ assemblyFile.asciidoc.attributes['source-highlighter'] = sourceHighlighter
116
+ } else if (assemblyModel.filetype !== 'html') {
117
+ delete assemblyFile.asciidoc.attributes['source-highlighter']
134
118
  }
135
- accum.push(assemblyFile)
136
- return true
119
+ return !!accum.push(assemblyFile)
137
120
  }, false)
138
- // NOTE restore source highlighter for conversion if defined in Assembler config
139
- if (sourceHighlighter !== undefined) {
140
- mergedAsciiDocAttributes['source-highlighter'] = sourceHighlighter
141
- } else if (assemblyModel.filetype !== 'html') {
142
- delete mergedAsciiDocAttributes['source-highlighter']
143
- }
144
121
  return accum
145
122
  },
146
123
  []
@@ -17,6 +17,7 @@ function selectMutableAttributes (loadAsciiDoc, contentCatalog, referencePage, a
17
17
  ]
18
18
  // we could consider using an Asciidoctor extension here to grab attributes passed via the API instead
19
19
  const immutableAttributeNames = doc.attribute_overrides.$keys()['$-'](additionalMutableNames)
20
+ immutableAttributeNames.push('doctype')
20
21
  return Object.entries(doc.getAttributes()).reduce((accum, [name, val]) => {
21
22
  if (!immutableAttributeNames.includes(name)) accum[name] = val
22
23
  return accum
@@ -0,0 +1,25 @@
1
+ 'use strict'
2
+
3
+ const HTML_RESERVED_ID_NAMES = 'content header footnotes footer footer-text premable toc toctitle'.split(' ')
4
+
5
+ function generateScopedId (componentSrc, componentVersion, separators, filetype, asXrefTarget) {
6
+ let { component, module: mod, relative } = componentSrc
7
+ let id = relative.replace(/\.adoc$/, '').replace(/[/.]/g, '-')
8
+ let { coordinate: coordinateSeparator, prefix: prefixSeparator } = separators
9
+ if (component !== componentVersion.name) {
10
+ id = [component, mod === 'ROOT' ? '' : mod, id].join(coordinateSeparator)
11
+ } else if (mod !== 'ROOT') {
12
+ if (asXrefTarget && coordinateSeparator === ':' && /(?:pass|stem)$/.test(mod) && /^[a-z]+(?:,[a-z-]+)*$/.test(id)) {
13
+ prefixSeparator = ''
14
+ mod = mod.replace(/(?:pass|stem)$/, '\\$&')
15
+ }
16
+ id = mod + coordinateSeparator + id
17
+ } else if (filetype === 'html' && HTML_RESERVED_ID_NAMES.includes(id)) {
18
+ id = prefixSeparator + id
19
+ prefixSeparator = ''
20
+ }
21
+ if (prefixSeparator && !/^[\p{Alpha}_:]/u.test(id)) id = prefixSeparator + id
22
+ return id
23
+ }
24
+
25
+ module.exports = generateScopedId
@@ -1,9 +1,11 @@
1
1
  'use strict'
2
2
 
3
3
  const createResourceKey = require('./create-resource-key')
4
- const generateId = require('./generate-id')
5
- const { resolveEmbedTarget, resolveLinkTarget } = require('./resolver')
4
+ const generateScopedId = require('./generate-scoped-id')
6
5
  const parseResourceRef = require('./parse-resource-ref')
6
+ const { resolveEmbedTarget, resolveLinkTarget } = require('./resolver')
7
+
8
+ const { NAMED_ID_ATTR_RX } = require('./rx')
7
9
 
8
10
  function rewriteXrefs (
9
11
  line,
@@ -11,15 +13,13 @@ function rewriteXrefs (
11
13
  assemblyModel,
12
14
  ctx,
13
15
  escapeForInline,
14
- idCoordinateSep,
15
- idScopeSep,
16
- idPrefix,
17
- idLeader,
18
16
  pagesInOutline,
17
+ idSeparators,
18
+ idLeader,
19
19
  doc,
20
20
  sourceLocation
21
21
  ) {
22
- return line.replace(/(?<![\\+])xref:((?:\.\/)?[\p{Alpha}0-9_/.{#].*?)\[(|.*?[^\\])\]/gu, (_, target, text) =>
22
+ return line.replace(/(?<![\\+])xref:((?:\.\/)?[\p{Alpha}0-9_/.{#].*?)\[(|[\s\S]*?[^\\])\]/gu, (_, target, text) =>
23
23
  rewriteXref(
24
24
  target,
25
25
  text,
@@ -27,11 +27,9 @@ function rewriteXrefs (
27
27
  assemblyModel,
28
28
  ctx,
29
29
  escapeForInline,
30
- idCoordinateSep,
31
- idScopeSep,
32
- idPrefix,
33
- idLeader,
34
30
  pagesInOutline,
31
+ idSeparators,
32
+ idLeader,
35
33
  doc,
36
34
  sourceLocation
37
35
  )
@@ -45,11 +43,9 @@ function rewriteXref (
45
43
  assemblyModel,
46
44
  ctx,
47
45
  escapeForInline,
48
- idCoordinateSep,
49
- idScopeSep,
50
- idPrefix,
51
- idLeader,
52
46
  pagesInOutline,
47
+ idSeparators,
48
+ idLeader,
53
49
  doc,
54
50
  sourceLocation
55
51
  ) {
@@ -66,7 +62,7 @@ function rewriteXref (
66
62
  } else {
67
63
  fragment = target
68
64
  }
69
- if (!resourceRef) return `xref:${idLeader || ''}${fragment}[${text}]`
65
+ if (!resourceRef) return `xref:${idLeader == null ? rawTarget : idLeader + fragment}[${text}]`
70
66
  const resourceId = parseResourceRef(resourceRef, ctx, 'page', contentCatalog)
71
67
  const family = resourceId.family
72
68
  let resource
@@ -75,7 +71,8 @@ function rewriteXref (
75
71
  const { linkReferenceStyle, pubRoot, siteRoot } = assemblyModel
76
72
  text ||= resource.asciidoc?.xreftext || rawTarget
77
73
  if (siteRoot || linkReferenceStyle === 'relative') {
78
- return `${resolveLinkTarget(resource, siteRoot, pubRoot, linkReferenceStyle, escapeForInline)}${fragment && '#' + fragment}[${text}]`
74
+ const hash = fragment && !(family === 'page' && fragment === resource.asciidoc.id) ? '#' + fragment : ''
75
+ return `${resolveLinkTarget(resource, siteRoot, pubRoot, linkReferenceStyle, escapeForInline)}${hash}[${text}]`
79
76
  }
80
77
  if (doc) {
81
78
  const msg = `Cannot create external ${family} reference in assembly because site URL is unknown: ${rawTarget}`
@@ -96,42 +93,41 @@ function rewriteXref (
96
93
  text = ''
97
94
  }
98
95
  const componentVersionCtx = { name: ctx.component, version: ctx.version }
99
- const refid = generateId(
96
+ const refid = generateScopedId(
100
97
  resource.src,
101
98
  componentVersionCtx,
102
- idCoordinateSep,
103
- idScopeSep,
104
- idPrefix,
105
- text.length > 0,
106
- fragment
107
- ).id
108
- return `xref:${refid}[${text}]`
99
+ idSeparators,
100
+ assemblyModel.filetype,
101
+ text.length > 0
102
+ )
103
+ return `xref:${fragment ? refid + idSeparators.scope + fragment : refid}[${text}]`
109
104
  }
110
105
 
111
106
  function rewriteImageAttr (val, contentCatalog, assemblyModel, ctx, assets) {
112
- const match = val.startsWith('image:') && /^image::?(.+?)\[(.*?)\]$/.exec(val)
107
+ const match = val.startsWith('image:') && /^image::?(.+?)\[(.*?)\](@|)$/.exec(val)
113
108
  if (!match) return
114
109
  const newTarget = rewriteImageRef(match[1], contentCatalog, assemblyModel, ctx, assets)
115
- return newTarget ? `image:${newTarget}[${match[2]}]` : val
110
+ return newTarget ? `image:${newTarget}[${match[2]}]${match[3]}` : val
116
111
  }
117
112
 
118
- function rewriteInlineImages (line, contentCatalog, assemblyModel, ctx, assets, escapeForInline) {
119
- return line.replace(/(?<![\\+])image:([^:\s[](?:[^[]*[^\s[])?)\[([^\]]*)\]/g, (m, target, attrlist) => {
120
- const newTarget = rewriteImageRef(target, contentCatalog, assemblyModel, ctx, assets, escapeForInline)
113
+ function rewriteInlineImages (line, contentCatalog, assemblyModel, ctx, assets, escapeForInline, doc) {
114
+ return line.replace(/(?<![\\+])image:([^:\s[](?:[^[]*[^\s[])?)\[(|[\s\S]*?[^\\])\]/g, (m, target, attrlist) => {
115
+ const newTarget = rewriteImageRef(target, contentCatalog, assemblyModel, ctx, assets, escapeForInline, doc)
121
116
  return newTarget ? `image:${newTarget}[${attrlist}]` : m
122
117
  })
123
118
  }
124
119
 
125
- function rewriteImageRef (target, contentCatalog, assemblyModel, ctx, assets, escapeForInline = false) {
120
+ function rewriteImageRef (target, contentCatalog, assemblyModel, ctx, assets, escapeForInline, doc) {
121
+ if (doc && ~target.indexOf('{')) target = doc.$sub_attributes(target, global.Opal.hash({ attribute_missing: 'skip' }))
126
122
  const image = isResourceRef(target) && contentCatalog.resolveResource(target, ctx, 'image', ['image'])
127
123
  if (!image?.out) return
128
124
  let newTarget
129
125
  const { filetype, embedReferenceStyle, linkReferenceStyle, outDirname, pubRoot, siteRoot } = assemblyModel
130
126
  if (filetype !== 'html') {
131
- newTarget = resolveEmbedTarget(image, outDirname, embedReferenceStyle, escapeForInline)
127
+ newTarget = resolveEmbedTarget(image, outDirname, embedReferenceStyle, escapeForInline ?? false)
132
128
  assets.add(image)
133
129
  } else if (siteRoot || linkReferenceStyle === 'relative') {
134
- newTarget = resolveLinkTarget(image, siteRoot, pubRoot, linkReferenceStyle, escapeForInline, false)
130
+ newTarget = resolveLinkTarget(image, siteRoot, pubRoot, linkReferenceStyle, escapeForInline ?? false, false)
135
131
  if (linkReferenceStyle === 'relative') assets.add(image)
136
132
  }
137
133
  if (!newTarget) return
@@ -160,6 +156,8 @@ function rewriteStyleAttribute (block, lines, idx, idLeader, replacementStyle =
160
156
  if (cellSpec) {
161
157
  prevLine = cellSpec[2]
162
158
  cellSpec = cellSpec[1]
159
+ } else if (~prevLine.indexOf('id=')) {
160
+ prevLine = `[${prevLine.slice(1, -1).replace(NAMED_ID_ATTR_RX, '')}]`
163
161
  }
164
162
  let rawStyle
165
163
  const commaIdx = prevLine.indexOf(',')
package/lib/util/rx.js ADDED
@@ -0,0 +1,5 @@
1
+ 'use strict'
2
+
3
+ const NAMED_ID_ATTR_RX = /(?:^|, *)id=(?:("[^"]*"|'[^']*'|[^,]*))/
4
+
5
+ module.exports = { NAMED_ID_ATTR_RX }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antora/assembler",
3
- "version": "1.0.0-beta.17",
3
+ "version": "1.0.0-beta.19",
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,6 +27,7 @@
27
27
  ".": "./lib/index.js",
28
28
  "./filter-component-versions": "./lib/filter-component-versions.js",
29
29
  "./load-config": "./lib/load-config.js",
30
+ "./log-command": "./lib/log-command.js",
30
31
  "./parse-resource-ref": "./lib/util/parse-resource-ref.js",
31
32
  "./produce-assembly-file": "./lib/produce-assembly-file.js",
32
33
  "./produce-assembly-files": "./lib/produce-assembly-files.js",
@@ -35,12 +36,12 @@
35
36
  },
36
37
  "imports": {
37
38
  "#asciidoctor-log-adapter": "./adapters/asciidoctor/jsonl-logger.rb",
38
- "#run-command": "@antora/run-command-helper",
39
39
  "#unconvert-inline-asciidoc": "./lib/util/unconvert-inline-asciidoc.js"
40
40
  },
41
41
  "dependencies": {
42
42
  "@asciidoctor/reducer": "~1.1",
43
43
  "@antora/expand-path-helper": "~3.0",
44
+ "@antora/run-command-helper": "~1.0",
44
45
  "braces": "~3.0",
45
46
  "picomatch": "~3.0",
46
47
  "js-yaml": "~4.1"
@@ -1,25 +0,0 @@
1
- 'use strict'
2
-
3
- const ReservedIdNames = 'content header footnotes footer footer-text premable toc toctitle'.split(' ')
4
-
5
- function generateId (componentSrc, componentVersion, coordinateSep, scopeSep, prefix, asXrefTarget, fragment) {
6
- let { component, module: mod, relative } = componentSrc
7
- let id = relative.replace(/\.adoc$/, '').replace(/[/.]/g, '-')
8
- if (component !== componentVersion.name) {
9
- id = [component, mod === 'ROOT' ? '' : mod, id].join(coordinateSep)
10
- } else if (mod !== 'ROOT') {
11
- if (asXrefTarget && coordinateSep === ':' && /(?:pass|stem)$/.test(mod) && /^[a-z]+(?:,[a-z-]+)*$/.test(id)) {
12
- prefix = ''
13
- mod = mod.replace(/(?:pass|stem)$/, '\\$&')
14
- }
15
- id = mod + coordinateSep + id
16
- } else if (ReservedIdNames.includes(id)) {
17
- id += scopeSep
18
- scopeSep = ''
19
- }
20
- if (prefix && !/^[\p{Alpha}_:]/u.test(id)) id = prefix + id
21
- const idLeader = id + scopeSep
22
- return { idLeader, id: fragment ? idLeader + fragment : id }
23
- }
24
-
25
- module.exports = generateId