@antora/assembler 1.0.0-beta.16 → 1.0.0-beta.18

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.
@@ -57,12 +57,12 @@ async function assembleContent (playbook, contentCatalog, converter, { configSou
57
57
  intrinsicAttributes[`assembler-filetype-${targetFiletype}`] = ''
58
58
  intrinsicAttributes['assembler-filetype'] = targetFiletype
59
59
  }
60
- Object.assign(assemblerConfig.asciidoc.attributes, intrinsicAttributes)
60
+ Object.assign(assemblerConfig.assembly.attributes, intrinsicAttributes)
61
61
  const assemblyFiles = produceAssemblyFiles(
62
62
  loadAsciiDoc,
63
63
  contentCatalog,
64
64
  assemblerConfig,
65
- createResolveAssemblyModel(context, contentCatalog, assemblyConfig, intrinsicAttributes, navigationCatalog)
65
+ generateSelectAssemblyProfile(context, contentCatalog, assemblyConfig, intrinsicAttributes, navigationCatalog)
66
66
  )
67
67
  if (!(assemblyFiles.length && typeof convert === 'function')) return assemblyFiles
68
68
  if (buildConfig.command == null && typeof getDefaultCommand === 'function') {
@@ -123,24 +123,33 @@ async function assembleContent (playbook, contentCatalog, converter, { configSou
123
123
  })
124
124
  }
125
125
 
126
- function createResolveAssemblyModel (context, contentCatalog, common, intrinsicAttributes, navigationCatalog) {
126
+ /**
127
+ * Generates a function that selects the active assembly profile, initializes an assembly model using
128
+ * the keys from the profile as well as any inherited shared keys, builds the navigation for the assembly, and
129
+ * returns the initialized assembly model. The assembly model is further populated after the call to this function.
130
+ */
131
+ function generateSelectAssemblyProfile (context, contentCatalog, base, intrinsicAttributes, navigationCatalog) {
127
132
  const logger = context.getLogger?.(PACKAGE_NAME)
128
133
  const { assemblerProfiles } = context.getVariables()
129
134
  if (!assemblerProfiles) {
130
135
  return (componentVersion) => {
131
136
  const navigation =
132
137
  navigationCatalog?.getNavigation(componentVersion.name, componentVersion.version) ?? componentVersion.navigation
133
- return Object.assign({ logger }, common, { navigation })
138
+ return Object.assign({}, base, { attributes: Object.assign({}, base.attributes), navigation, logger })
134
139
  }
135
140
  }
136
141
  const boundSendToLog = sendToLog.bind(logger)
137
142
  const { buildNavigation = require('@antora/navigation-builder') } = context.getFunctions()
138
143
  return (componentVersion) => {
144
+ const attributes = Object.assign({}, base.attributes)
139
145
  const componentVersionProfiles = assemblerProfiles.get(componentVersion.version + '@' + componentVersion.name)
140
146
  const overrides =
141
147
  componentVersionProfiles?.get(intrinsicAttributes['assembler-profile']) ?? componentVersionProfiles?.get() ?? {}
142
148
  const { navFiles, messages } = overrides
143
- const model = Object.assign({ logger }, common, overrides)
149
+ Object.entries(overrides.attributes ?? {}).forEach(([name, val]) => {
150
+ attributes[name] = val
151
+ })
152
+ const model = Object.assign({}, base, overrides, { attributes, logger })
144
153
  delete model.navFiles
145
154
  delete model.messages
146
155
  const navigationOverride = navigationCatalog?.getNavigation(componentVersion.name, componentVersion.version)
package/lib/configure.js CHANGED
@@ -16,8 +16,17 @@ function internalConfigure (converter, config = {}, providers = {}) {
16
16
 
17
17
  this.once('navigationBuilt', async ({ playbook, contentCatalog }) => {
18
18
  const { assembleContent = require('./assemble-content'), ...assembleContentConfig } = providers
19
- assembleContentConfig.configSource ??= config.configFile
20
- await assembleContent.call(this, playbook, contentCatalog, converter, assembleContentConfig)
19
+ if (assembleContentConfig.configSource?.constructor === Object) {
20
+ await assembleContent.call(this, playbook, contentCatalog, converter, assembleContentConfig)
21
+ } else {
22
+ const singleConfig = !('configFiles' in config)
23
+ const configFiles = singleConfig ? config.configFile : config.configFiles
24
+ for (const configSource of Array.isArray(configFiles) ? configFiles : [configFiles]) {
25
+ const assembleContentConfigWithConfigSource = Object.assign({}, assembleContentConfig, { configSource })
26
+ await assembleContent.call(this, playbook, contentCatalog, converter, assembleContentConfigWithConfigSource)
27
+ if (singleConfig) break
28
+ }
29
+ }
21
30
  })
22
31
  }
23
32
 
@@ -14,6 +14,7 @@ const ASSEMBLY_KEYS = [
14
14
  'linkReferenceStyle',
15
15
  'dropExplicitXrefText',
16
16
  ]
17
+ const CAMEL_CASE_STOP_PATHS = ['asciidoc.attributes', 'assembly.attributes']
17
18
 
18
19
  function loadConfig (playbook, configSource) {
19
20
  let resolvedConfigSource
@@ -38,19 +39,11 @@ function loadConfig (playbook, configSource) {
38
39
  return fsp
39
40
  .readFile(resolvedConfigSource)
40
41
  .then((data) =>
41
- Object.assign(camelCaseKeys(yaml.load(data), ['asciidoc']), { file: resolvedConfigSource })
42
+ Object.assign(camelCaseKeys(yaml.load(data), CAMEL_CASE_STOP_PATHS), { file: resolvedConfigSource })
42
43
  )
43
44
  })
44
45
  ).then((config) => {
45
46
  if (config.enabled === false) return config
46
- let asciidocAttrs
47
- if (!config.asciidoc) {
48
- config.asciidoc = { attributes: (asciidocAttrs = {}) }
49
- } else if (!(asciidocAttrs = config.asciidoc.attributes)) {
50
- config.asciidoc.attributes = asciidocAttrs = {}
51
- }
52
- if (!('revdate' in asciidocAttrs)) asciidocAttrs.revdate = getLocalDate()
53
- asciidocAttrs['page-partial'] = null
54
47
  const remapComponentVersionsKey = !('componentVersionFilter' in config)
55
48
  const componentVersionFilter = (config.componentVersionFilter ??= {})
56
49
  if (remapComponentVersionsKey && 'componentVersions' in config) {
@@ -71,8 +64,24 @@ function loadConfig (playbook, configSource) {
71
64
  delete config[key]
72
65
  }
73
66
  }
74
- if (!('doctype' in assembly)) assembly.doctype = 'doctype' in asciidocAttrs ? asciidocAttrs.doctype : 'book'
75
- delete asciidocAttrs.doctype
67
+ let assemblyAttrs
68
+ if ('attributes' in assembly) {
69
+ assemblyAttrs = assembly.attributes ?? {}
70
+ if ('asciidoc' in config) delete config.asciidoc
71
+ } else if ('asciidoc' in config) {
72
+ assemblyAttrs = assembly.attributes = config.asciidoc?.attributes ?? {}
73
+ delete config.asciidoc
74
+ } else {
75
+ assemblyAttrs = assembly.attributes = {}
76
+ }
77
+ assemblyAttrs['page-partial'] = null
78
+ config.asciidoc = Object.defineProperty({}, 'attributes', {
79
+ get: function () {
80
+ return this.assembly.attributes
81
+ }.bind(config),
82
+ })
83
+ if (!('doctype' in assembly)) assembly.doctype = 'doctype' in assemblyAttrs ? assemblyAttrs.doctype : 'book'
84
+ delete assemblyAttrs.doctype
76
85
  if (!('rootLevel' in assembly)) assembly.rootLevel = 0
77
86
  if (!('insertStartPage' in assembly)) assembly.insertStartPage = true
78
87
  if (['discrete', 'fuse', 'enclose'].indexOf(assembly.sectionMergeStrategy) < 0) {
@@ -84,6 +93,7 @@ function loadConfig (playbook, configSource) {
84
93
  if (['always', 'if-redundant', 'never'].indexOf(assembly.dropExplicitXrefText) < 0) {
85
94
  assembly.dropExplicitXrefText = 'never'
86
95
  }
96
+ assembly.revdate = getLocalDate()
87
97
  const build = (config.build ??= {})
88
98
  if (build.dir === '$' + '{playbook.output.dir}') {
89
99
  throw new Error('Not implemented')
@@ -1,8 +1,11 @@
1
1
  'use strict'
2
2
 
3
3
  const createAsciiDocFile = require('./util/create-asciidoc-file')
4
- const parseResourceRef = require('./util/parse-resource-ref')
5
- const path = require('node:path/posix')
4
+ const createResourceKey = require('./util/create-resource-key')
5
+ const generateScopedId = require('./util/generate-scoped-id')
6
+ const { resolveLinkTarget } = require('./util/resolver')
7
+ const { rewriteXrefs, rewriteImageRef, rewriteInlineImages, rewriteStyleAttribute } = require('./util/rewriter')
8
+ const { NamedIdAttrRx } = require('./util/rx')
6
9
  const sanitize = require('./util/sanitize')
7
10
  const unconvertInlineAsciiDoc = require('./util/unconvert-inline-asciidoc')
8
11
 
@@ -10,7 +13,6 @@ const AttributeEntryRx = /^:([^:-][^:]*):(?: .*)?$/
10
13
  const BuiltInNamedEntities = { amp: '&', apos: "'", gt: '>', lt: '<', nbsp: ' ', quot: '"' }
11
14
  const CharRefRx = /&(?:([a-z][a-z]+\d{0,2})|#(?:(\d{2,6})|x([a-z\d]{2,5})));/g
12
15
  const DiscardAttributes = 'doctype leveloffset assembly-navtitle assembly-style underscore'.split(' ')
13
- const ReservedIdNames = 'content header footnotes footer footer-text premable toc toctitle'.split(' ')
14
16
 
15
17
  function produceAssemblyFile (
16
18
  loadAsciiDoc,
@@ -24,7 +26,14 @@ function produceAssemblyFile (
24
26
  ) {
25
27
  const pagesByUrl = files.reduce((map, it) => (it.src.family === 'page' ? map.set(it.pub.url, it) : map), new Map())
26
28
  if (outline.urlType === 'internal' && !pagesByUrl.get(outline.url) && !(outline.items || []).length) return
27
- const pagesInOutline = selectPagesInOutline(outline, pagesByUrl, componentVersion)
29
+ const pagesInOutline = selectPagesInOutline(outline, pagesByUrl)
30
+ pagesInOutline.generateIdFromTitle = generateIdFromTitle.bind(
31
+ loadAsciiDoc(
32
+ { contents: Buffer.alloc(0), src: { family: 'page', relative: 'generate-id.adoc' } },
33
+ undefined,
34
+ asciidocConfig
35
+ )
36
+ )
28
37
  const buffer = mergeAsciiDoc(
29
38
  loadAsciiDoc,
30
39
  contentCatalog,
@@ -63,29 +72,30 @@ function buildAsciiDocHeader (componentVersion, navtitle, assemblyModel) {
63
72
  let doctitle = navtitleAsciiDoc
64
73
  if (navtitlePlain !== componentVersion.title) doctitle = `${componentVersion.title}: ${doctitle}`
65
74
  const version = componentVersion.version === 'master' ? '' : componentVersion.version
75
+ const displayVersion = componentVersion.displayVersion
66
76
  const buffer = [
67
77
  `= ${doctitle}`,
68
- ...(version ? [`:revnumber: ${version}`] : []),
78
+ ...(version ? [`:revnumber: ${displayVersion}`] : []),
69
79
  ...(doctype === 'article' ? [] : [`:doctype: ${doctype ?? 'book'}`]),
70
80
  ':underscore: _',
71
81
  // Q: should we pass these via the CLI so they cannot be modified?
72
82
  `:page-component-name: ${componentVersion.name}`,
73
83
  `:page-component-version:${version ? ' ' + version : ''}`,
74
84
  ':page-version: {page-component-version}',
75
- `:page-component-display-version: ${componentVersion.displayVersion}`,
85
+ `:page-component-display-version: ${displayVersion}`,
76
86
  `:page-component-title: ${componentVersion.title}`,
77
87
  ]
78
88
  return Object.assign(buffer, { navtitle })
79
89
  }
80
90
 
81
- function selectPagesInOutline (outlineEntry, pagesByUrl, componentVersion, accum) {
91
+ function selectPagesInOutline (outlineEntry, pagesByUrl, accum) {
82
92
  accum ??= Object.assign(new Map(), { assembled: { pages: new Map(), assets: new Set() } })
83
93
  const page = outlineEntry.urlType === 'internal' ? pagesByUrl.get(outlineEntry.url) : undefined
84
94
  if (page) {
85
95
  accum.set(createResourceKey(page.src), page)
86
96
  accum.set(outlineEntry.url, page)
87
97
  }
88
- for (const item of outlineEntry.items || []) selectPagesInOutline(item, pagesByUrl, componentVersion, accum)
98
+ for (const item of outlineEntry.items || []) selectPagesInOutline(item, pagesByUrl, accum)
89
99
  return accum
90
100
  }
91
101
 
@@ -113,26 +123,19 @@ function mergeAsciiDoc (
113
123
  let navtitlePlain = sanitize(navtitle)
114
124
  let navtitleAsciiDoc = unconvertInlineAsciiDoc(navtitle)
115
125
  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
- logger,
125
- } = assemblyModel
126
+ const { doctype, filetype, linkReferenceStyle, pubRoot, siteRoot, xmlIds, logger } = assemblyModel
127
+ const assembled = pagesInOutline.assembled
126
128
  // FIXME: ideally, resource ID would be stored in navigation so we can look up the page more efficiently
127
129
  const page = urlType === 'internal' && !unresolved ? pagesInOutline.get(url) : undefined
128
130
  const atDocumentRoot = !buffer.inBody
129
131
  const atBookRoot = atDocumentRoot && !level && doctype === 'book' && (supportsParts = true)
130
132
  const hasItems = items.length > 0
131
- const pubRoot = outDirname ? '/' + outDirname : ''
133
+ const idPrefix =
134
+ asciidocConfig.attributes['assembler-idprefix'] ?? ('assembler-idprefix' in asciidocConfig.attributes ? '' : '_')
132
135
  const idSeparator = xmlIds ? '-' : ':'
133
136
  const idScopeSeparator = idSeparator.repeat(3)
134
137
  const idCoordinateSeparator = idSeparator === '-' ? '----' : idSeparator
135
- if (page && !pagesInOutline.assembled.pages.has(page)) {
138
+ if (page && !assembled.pages.has(page)) {
136
139
  const contents = page.src.contents
137
140
  if (contents == null) {
138
141
  buffer.inBody ??= false
@@ -143,16 +146,15 @@ function mergeAsciiDoc (
143
146
  Object.assign({}, page, { contents: trimAsciiDoc(contents), mediaType })
144
147
  )
145
148
  const doc = loadAsciiDoc(pageAsAsciiDoc, contentCatalog, asciidocConfig)
146
- let asciidoctorLogger = doc.getLogger()
147
- if (logger) {
148
- asciidoctorLogger = Object.assign(asciidoctorLogger.$dup(), {
149
- delegate: {
150
- warn () {
151
- return logger.warn.apply(logger, arguments)
149
+ doc.logger = logger
150
+ ? Object.assign(doc.getLogger().$dup(), {
151
+ delegate: {
152
+ warn () {
153
+ return logger.warn.apply(logger, arguments)
154
+ },
152
155
  },
153
- },
154
- })
155
- }
156
+ })
157
+ : doc.getLogger()
156
158
  if (doc.hasAttribute('assembly-navtitle')) {
157
159
  navtitleAsciiDoc = doc.getAttribute('assembly-navtitle')
158
160
  navtitlePlain = sanitize((navtitle = doc.$apply_reftext_subs(navtitleAsciiDoc)))
@@ -180,7 +182,14 @@ function mergeAsciiDoc (
180
182
  }
181
183
  // NOTE: in Antora, docname is relative src path from module without file extension
182
184
  const docname = doc.getAttribute('docname')
183
- const { idPrefix, id: idScope } = generateId(page.src, componentVersion, idCoordinateSeparator, idScopeSeparator)
185
+ const { idLeader: idScopeLeader, id: idScope } = generateScopedId(
186
+ page.src,
187
+ componentVersion,
188
+ idCoordinateSeparator,
189
+ idScopeSeparator,
190
+ idPrefix,
191
+ filetype
192
+ )
184
193
  let pageFragment = ''
185
194
  let pageRoles = ''
186
195
  let pageStyle = doc.getAttribute('assembly-style', '')
@@ -236,7 +245,7 @@ function mergeAsciiDoc (
236
245
  assemblyModel = Object.assign({}, assemblyModel, { sectionMergeStrategy: 'discrete' })
237
246
  } else {
238
247
  if (atDocumentRoot) {
239
- pageFragment = `#${idPrefix}${doc.getId() ?? pageStyle}`
248
+ pageFragment = `#${idScopeLeader}${doc.getId() ?? pageStyle}`
240
249
  buffer.unshift(`[#${idScope}]`)
241
250
  } else {
242
251
  pageFragment = `#${idScope}`
@@ -269,28 +278,18 @@ function mergeAsciiDoc (
269
278
  //if (overviewTitle === navtitle) overviewTitle = doc.getAttribute('overview-title', 'Overview')
270
279
  const overviewTitle = doc.getAttribute('overview-title', 'Overview')
271
280
  buffer.push('')
272
- // NOTE: try to toggle sectids; otherwise, fallback to globally unique synthetic ID
273
- let toggleSectids, syntheticId
274
- if (doc.isAttribute('sectids')) {
275
- if (doc.isAttributeLocked('sectids')) {
276
- syntheticId = `__object-id-${getObjectId(outlineEntry)}`
277
- } else {
278
- buffer.push(':!sectids:')
279
- toggleSectids = true
280
- }
281
- }
281
+ const syntheticId = pagesInOutline.generateIdFromTitle(navtitleAsciiDoc, idCoordinateSeparator)
282
282
  let hlevel = level + 2
283
283
  if (hlevel > 6) {
284
284
  const blockStyle = `discrete.h${hlevel}`
285
285
  hlevel = 6
286
- buffer.push(syntheticId ? `[${blockStyle}#${syntheticId}]` : `[${blockStyle}]`)
287
- } else if (syntheticId) {
286
+ buffer.push(`[${blockStyle}#${syntheticId}]`)
287
+ } else {
288
288
  buffer.push(`[#${syntheticId}]`)
289
289
  }
290
290
  buffer.push(`${'='.repeat(hlevel)} ${overviewTitle}`)
291
- if (toggleSectids) buffer.push(':sectids:')
292
291
  }
293
- pagesInOutline.assembled.pages.set(page, pageFragment)
292
+ assembled.pages.set(page, pageFragment)
294
293
  if (doc.hasSections()) {
295
294
  fixSectionLevels(doc.getSections(), atBookRoot && !pageStyle ? undefined : nextSectionLevel)
296
295
  }
@@ -318,10 +317,6 @@ function mergeAsciiDoc (
318
317
  }
319
318
  }
320
319
  const refs = doc.getCatalog().refs
321
- const scannedInlineAnchors = Object.values(refs.$$smap).reduce((accum, it) => {
322
- if (it.node_name === 'inline_anchor') accum[it.id] = it
323
- return accum
324
- }, {})
325
320
  allBlocks.forEach((block) => {
326
321
  const contentModel = block.content_model
327
322
  if (
@@ -380,112 +375,33 @@ function mergeAsciiDoc (
380
375
  if (!refs['$key?'](refid) && (~refid.indexOf(' ') || refid.toLowerCase() !== refid)) {
381
376
  if ((refid = doc.$resolve_id(refid))['$nil?']()) return m
382
377
  }
383
- const alphaPrefix = !(refid in scannedInlineAnchors) || /^[\p{Alpha}_:]/u.test(idPrefix) ? '' : 'iid-'
384
- return `<<${alphaPrefix}${idPrefix}${refid}${text ? ',' + text : ''}>>`
378
+ return `<<${idScopeLeader}${refid}${text ? ',' + text : ''}>>`
385
379
  })
386
380
  }
387
381
  // NOTE: the next check takes care of inline and block anchors
388
382
  if (~line.indexOf('[[')) {
389
- line = line.replace(/\[\[([\p{Alpha}_:][\p{Alpha}0-9_\-:.]*)(|, *.+?)\]\]/gu, (m, refid, text) => {
390
- const alphaPrefix = !(refid in scannedInlineAnchors) || /^[\p{Alpha}_:]/u.test(idPrefix) ? '' : 'iid-'
391
- return `[[${alphaPrefix}${idPrefix}${refid}${text}]]`
383
+ line = line.replace(/\[\[([\p{Alpha}_:][\p{Alpha}0-9_\-:.]*)(|, *.+?)\]\]/gu, (_, refid, text) => {
384
+ return `[[${idScopeLeader}${refid}${text}]]`
392
385
  })
393
386
  }
394
387
  if (~line.indexOf('xref:')) {
395
- // Q: should we allow : as first character of target?
396
- line = line.replace(/(?<![\\+])xref:((?:\.\/)?[\p{Alpha}0-9_/.{#].*?)\[(|.*?[^\\])\]/gu, (_, target_, text) => {
397
- let target = target_
398
- if (~target.indexOf('{')) target = doc.$sub_attributes(target, Opal.hash({ attribute_missing: 'skip' }))
399
- let fragment, resource, resourceRef
400
- const hashIdx = target.indexOf('#')
401
- if (~hashIdx) {
402
- resourceRef = target.slice(0, hashIdx)
403
- fragment = target.slice(hashIdx + 1)
404
- } else if (target.endsWith('.adoc') || ~target.indexOf('$')) {
405
- resourceRef = target
406
- fragment = ''
407
- } else {
408
- fragment = target
409
- }
410
- // Q: should we validate the internal ID here?
411
- if (!resourceRef) return `xref:${idPrefix}${fragment}[${text}]`
412
- const resourceId = parseResourceRef(resourceRef, page.src, 'page', contentCatalog)
413
- if (resourceId.family !== 'page' || !(resource = pagesInOutline.get(createResourceKey(resourceId)))) {
414
- if ((resource = contentCatalog.getById(resourceId))?.pub) {
415
- text ||= resource.asciidoc?.xreftext || target_
416
- if (siteRoot || linkRefStyle === 'relative') {
417
- return `${resolveLinkTarget(resource, siteRoot, pubRoot, linkRefStyle)}${fragment && '#' + fragment}[${text}]`
418
- }
419
- asciidoctorLogger.warn(
420
- doc.createLogMessage(
421
- `Cannot create external ${resourceId.family} reference in assembly because site URL is unknown: ${target_}`,
422
- { source_location: { file: relative, lineno: idx + 1 } }
423
- )
424
- )
425
- }
426
- const linkAttrlist = text
427
- ? (~text.indexOf(',') ? `"${text}"` : text) + ',role=unresolved'
428
- : (~target.indexOf(':') ? `${target},` : '') + 'role=unresolved'
429
- return `link:${target_}[${linkAttrlist}]`
430
- }
431
- if (fragment === resource.asciidoc.id) fragment = ''
432
- if (
433
- text &&
434
- (assemblyModel.dropExplicitXrefText === 'always' ||
435
- (assemblyModel.dropExplicitXrefText === 'if-redundant' && text === resource.title))
436
- ) {
437
- text = ''
438
- }
439
- const refid = generateId(
440
- resource.src,
441
- componentVersion,
442
- idCoordinateSeparator,
443
- idScopeSeparator,
444
- text.length > 0,
445
- fragment
446
- ).id
447
- return `xref:${refid}[${text}]`
448
- })
449
- }
450
- if (~line.indexOf('link:{attachmentsdir}/')) {
451
- line = line.replace(/(?<![\\+])link:\{attachmentsdir\}\/([^\s[]+)\[(|.*?[^\\])\]/g, (m, relative, text) => {
452
- const attachment =
453
- (siteRoot || linkRefStyle === 'relative') &&
454
- contentCatalog.getById({
455
- component: componentVersion.name,
456
- version: componentVersion.version,
457
- module: module_,
458
- family: 'attachment',
459
- relative,
460
- })
461
- // TODO: handle unresolved attachment page
462
- return attachment?.out ? `${resolveLinkTarget(attachment, siteRoot, pubRoot, linkRefStyle)}[${text}]` : m
463
- })
388
+ line = rewriteXrefs(
389
+ line,
390
+ contentCatalog,
391
+ assemblyModel,
392
+ page.src,
393
+ true,
394
+ idCoordinateSeparator,
395
+ idScopeSeparator,
396
+ idPrefix,
397
+ idScopeLeader,
398
+ pagesInOutline,
399
+ doc,
400
+ { file: relative, lineno: idx + 1 }
401
+ )
464
402
  }
465
403
  if (~line.indexOf('image:') && !line.startsWith('image::')) {
466
- line = line.replace(/(?<![\\+])image:([^:\s[](?:[^[]*[^\s[])?)\[([^\]]*)\]/g, (m, target, attrlist) => {
467
- let image
468
- if (
469
- isResourceSpec(target) &&
470
- (image = contentCatalog.resolveResource(target, page.src, 'image', ['image']))?.out
471
- ) {
472
- if (filetype !== 'html') {
473
- pagesInOutline.assembled.assets.add(image)
474
- return `image:${resolveEmbedTarget(image, outDirname, embedRefStyle, true)}[${attrlist}]`
475
- }
476
- if (siteRoot || linkRefStyle === 'relative') {
477
- pagesInOutline.assembled.assets.add(image)
478
- return `image:${resolveLinkTarget(image, siteRoot, pubRoot, linkRefStyle)}[${attrlist}]`
479
- }
480
- asciidoctorLogger.warn(
481
- doc.createLogMessage(
482
- `Cannot create external image reference in assembly because site URL is unknown: ${target}`,
483
- { source_location: { file: relative, lineno: idx + 1 } }
484
- )
485
- )
486
- }
487
- return m
488
- })
404
+ line = rewriteInlineImages(line, contentCatalog, assemblyModel, page.src, assembled.assets, true)
489
405
  }
490
406
  lines[idx] = line
491
407
  }
@@ -511,7 +427,7 @@ function mergeAsciiDoc (
511
427
  return '='.repeat(targetMarkerLength) + ' ' + rest
512
428
  })
513
429
  // NOTE: ID will be undefined if sectids are turned off
514
- if (block.getId()) rewriteStyleAttribute(block, lines, idx, idPrefix, blockStyle)
430
+ if (block.getId()) rewriteStyleAttribute(block, lines, idx, idScopeLeader, blockStyle)
515
431
  } else {
516
432
  if (context === 'image') {
517
433
  let line = lines[idx] || ''
@@ -530,27 +446,17 @@ function mergeAsciiDoc (
530
446
  imageMacroOffset = -1
531
447
  }
532
448
  }
533
- if (imageMacroOffset >= 0) {
449
+ if (~imageMacroOffset) {
534
450
  const target = block.getAttribute('target')
535
- if (isResourceSpec(target)) {
536
- const image = contentCatalog.resolveResource(target, page.src, 'image', ['image'])
537
- // FIXME: handle (or report) case when image is not resolved
538
- if (image?.out && (filetype !== 'html' || siteRoot)) {
539
- const attrlist = line.slice(line.indexOf('[') + 1, -1)
540
- pagesInOutline.assembled.assets.add(image)
541
- lines[idx] =
542
- filetype === 'html'
543
- ? `${prefix}image::${resolveLinkTarget(image, siteRoot, pubRoot, linkRefStyle, false)}[${attrlist}]`
544
- : `${prefix}image::${resolveEmbedTarget(image, outDirname, embedRefStyle)}[${attrlist}]`
545
- }
546
- }
451
+ const newTarget = rewriteImageRef(target, contentCatalog, assemblyModel, page.src, assembled.assets)
452
+ if (newTarget) lines[idx] = `${prefix}image::${newTarget}${line.slice(line.indexOf('['))}`
547
453
  lastImageMacroAt = [idx, imageMacroOffset]
548
454
  }
549
455
  } else if (context === 'document' && block.hasHeader()) {
550
456
  // nested document
551
457
  idx = (block.getHeader().getLineNumber() || idx + 1) - 1
552
458
  }
553
- if (block.getId()) rewriteStyleAttribute(block, lines, idx, idPrefix)
459
+ if (block.getId()) rewriteStyleAttribute(block, lines, idx, idScopeLeader)
554
460
  }
555
461
  })
556
462
  safePush(
@@ -585,20 +491,7 @@ function mergeAsciiDoc (
585
491
  } else {
586
492
  buffer.inBody = true
587
493
  buffer.push('')
588
- // NOTE: try to toggle sectids; otherwise, fallback to globally unique synthetic ID
589
- // Q: should we unset docname, page-module, etc?
590
- let toggleSectids, syntheticId
591
- if (!('sectids' in asciidocConfig.attributes)) {
592
- buffer.push(':!sectids:')
593
- toggleSectids = true
594
- } else if (typeof asciidocConfig.attributes.sectids === 'string') {
595
- if ('sectids' in mutableAttributes) {
596
- buffer.push(':!sectids:')
597
- toggleSectids = true
598
- } else {
599
- syntheticId = `__object-id-${global.Opal.hash(outlineEntry).$object_id()}`
600
- }
601
- }
494
+ const syntheticId = pagesInOutline.generateIdFromTitle(navtitleAsciiDoc, idCoordinateSeparator)
602
495
  let sectionTitle = navtitleAsciiDoc
603
496
  if (urlType === 'external') {
604
497
  sectionTitle = `${url}[${navtitleAsciiDoc.replace(/\]/g, '\\]')}]`
@@ -606,22 +499,29 @@ function mergeAsciiDoc (
606
499
  const resource = files.find((it) => it.pub.url === url)
607
500
  if (resource) {
608
501
  if (resource.src.family === 'page' && pagesInOutline.has(resource.pub.url)) {
609
- const refid = generateId(resource.src, componentVersion, idCoordinateSeparator, idScopeSeparator, true).id
502
+ const { id: refid } = generateScopedId(
503
+ resource.src,
504
+ componentVersion,
505
+ idCoordinateSeparator,
506
+ idScopeSeparator,
507
+ idPrefix,
508
+ filetype,
509
+ true
510
+ )
610
511
  sectionTitle = `xref:${refid}[${navtitleAsciiDoc.replace(/\]/g, '\\]')}]`
611
512
  } else if (siteRoot) {
612
- sectionTitle = `${resolveLinkTarget(resource, siteRoot, pubRoot, linkRefStyle)}[${navtitleAsciiDoc.replace(/\]/g, '\\]')}]`
513
+ sectionTitle = `${resolveLinkTarget(resource, siteRoot, pubRoot, linkReferenceStyle, true)}[${navtitleAsciiDoc.replace(/\]/g, '\\]')}]`
613
514
  }
614
515
  }
615
516
  }
616
517
  let hlevel = level + 1
617
518
  if (hlevel > 6) {
618
519
  hlevel = 6
619
- buffer.push(syntheticId ? `[discrete#${syntheticId}]` : '[discrete]')
620
- } else if (syntheticId) {
520
+ buffer.push(`[discrete#${syntheticId}]`)
521
+ } else {
621
522
  buffer.push(`[#${syntheticId}]`)
622
523
  }
623
524
  buffer.push(`${'='.repeat(hlevel)} ${sectionTitle}`)
624
- if (toggleSectids) buffer.push(':sectids:')
625
525
  }
626
526
  }
627
527
 
@@ -697,6 +597,7 @@ function processDocumentHeader (doc, lines, buffer, ignoreLines) {
697
597
  .slice(1, -1)
698
598
  .trim()
699
599
  .replace(/(?:^\w[\w-]*(?!=|[\w-]))?([#.%]\w[\w-]*)*/, '')
600
+ .replace(NamedIdAttrRx, '')
700
601
  if (attrlist) buffer.push('[' + attrlist + ']')
701
602
  }
702
603
  } else if (belowDoctitle) {
@@ -732,64 +633,6 @@ function fixSectionLevels (sections, expectedLevel) {
732
633
  })
733
634
  }
734
635
 
735
- function rewriteStyleAttribute (block, lines, idx, idPrefix, replacementStyle = '') {
736
- let prevLine = lines[idx - 1]
737
- const char0 = prevLine?.charAt()
738
- if (char0) {
739
- if (
740
- (char0 === '.' && /^\.\.?[^ \t.]/.test(prevLine)) ||
741
- (char0 === '[' &&
742
- prevLine.charAt(1) === '[' &&
743
- /^\[\[(?:|[\p{Alpha}_:][\p{Alpha}0-9_\-:.]*(?:, *.+)?)\]\]$/u.test(prevLine))
744
- ) {
745
- return rewriteStyleAttribute(block, lines, idx - 1, idPrefix, replacementStyle)
746
- }
747
- }
748
- let cellSpec
749
- if (
750
- char0 &&
751
- (char0 === '[' || (block.getDocument().isNested() && (cellSpec = prevLine.match(/^([^[|]*)\| *(\[.+)/)))) &&
752
- prevLine.charAt(prevLine.length - 1) === ']'
753
- ) {
754
- if (cellSpec) {
755
- prevLine = cellSpec[2]
756
- cellSpec = cellSpec[1]
757
- }
758
- let rawStyle
759
- const commaIdx = prevLine.indexOf(',')
760
- if (~commaIdx) {
761
- rawStyle = prevLine.slice(1, commaIdx)
762
- if (~rawStyle.indexOf('=')) rawStyle = undefined
763
- } else if (!~prevLine.indexOf('=')) {
764
- rawStyle = prevLine.slice(1, prevLine.length - 1)
765
- }
766
- if (rawStyle) {
767
- if (~rawStyle.indexOf('#')) {
768
- prevLine = prevLine.replace(/#[^.%,\]]+/, `#${idPrefix}${block.getId()}`)
769
- if (replacementStyle) prevLine = prevLine.replace(/^[^#.%,\]]+/, `[${replacementStyle}`)
770
- } else {
771
- prevLine = `[${
772
- replacementStyle ? rawStyle.replace(/^[^.%]*/, replacementStyle) : rawStyle
773
- }#${idPrefix}${block.getId()}${prevLine.slice(rawStyle.length + 1)}`
774
- }
775
- } else {
776
- prevLine = `[${replacementStyle}#${idPrefix}${block.getId()}${rawStyle == null ? ',' : ''}${prevLine.slice(1)}`
777
- }
778
- if (cellSpec) prevLine = `${cellSpec}|${prevLine}`
779
- lines[idx - 1] = prevLine
780
- } else {
781
- lines.splice(idx, 0, `[${replacementStyle}#${idPrefix}${block.getId()}]`)
782
- }
783
- }
784
-
785
- function isResourceSpec (str) {
786
- return !(~str.indexOf(':') && (~str.indexOf('://') || (str.startsWith('data:') && ~str.indexOf(','))))
787
- }
788
-
789
- function getObjectId (obj) {
790
- return global.Opal.id(obj)
791
- }
792
-
793
636
  // NOTE: blank lines at top and bottom of document create mismatch when using line numbers to navigate source lines
794
637
  // IMPORTANT: this must not leave behind lines the parser will drop!
795
638
  // IDEA: another option is to capture initial lineno of reader and use as offset (but preseves those blank lines)
@@ -812,58 +655,11 @@ function safePush (onto, entries) {
812
655
  }
813
656
  }
814
657
 
815
- function resolveEmbedTarget (resource, outDirname, referenceStyle, escapeForInline) {
816
- const target =
817
- referenceStyle === 'output-relative' ? resource.out.path : path.relative(outDirname + '/', resource.out.path)
818
- return escapeForInline ? target.replace(/_/g, '{underscore}') : target
819
- }
820
-
821
- function resolveLinkTarget (resource, siteRoot, pubRoot, referenceStyle, escapeForInline = true) {
822
- let target
823
- if (resource.site?.url) {
824
- target = ['', resource.pub.url]
825
- } else {
826
- switch (referenceStyle) {
827
- case 'absolute':
828
- target = ['', siteRoot.url + resource.pub.url]
829
- break
830
- case 'root-relative':
831
- target = ['link:', siteRoot.path + resource.pub.url]
832
- break
833
- default:
834
- target = ['link:', computeRelativeUrl(pubRoot + '/', resource.pub.url)]
835
- }
836
- }
837
- if (escapeForInline) target[1] = target[1].replace(/_/g, '{underscore}')
838
- return target.join('')
839
- }
840
-
841
- function computeRelativeUrl (from, to) {
842
- const rel = path.relative(from, to)
843
- return to.charAt(to.length - 1) === '/' ? rel + '/' : rel
844
- }
845
-
846
- function createResourceKey ({ component, version, module: mod, family, relative }) {
847
- return `${version}@${component}:${mod === 'ROOT' ? '' : mod}:${family === 'page' ? '' : family + '$'}${relative}`
848
- }
849
-
850
- function generateId (componentSrc, componentVersion, coordinateSep, scopeSep, asXrefTarget, fragment) {
851
- let { component, module: mod, relative } = componentSrc
852
- let id = relative.replace(/\.adoc$/, '').replace(/[/.]/g, '-')
853
- if (component !== componentVersion.name) {
854
- id = [component, mod === 'ROOT' ? '' : mod, id].join(coordinateSep)
855
- } else if (mod !== 'ROOT') {
856
- if (asXrefTarget && coordinateSep === ':' && /(?:pass|stem)$/.test(mod) && /^[a-z]+(?:,[a-z-]+)*$/.test(id)) {
857
- mod = mod.replace(/(?:pass|stem)$/, '\\$&')
858
- }
859
- id = mod + coordinateSep + id
860
- } else if (ReservedIdNames.includes(id)) {
861
- id += scopeSep
862
- scopeSep = ''
863
- }
864
- const idPrefix = id + scopeSep
865
- if (fragment) id = idPrefix + fragment
866
- return { idPrefix, id }
658
+ function generateIdFromTitle (titleAsciiDoc, sep) {
659
+ const Section = this.$class().$const_get('::Asciidoctor::Section')
660
+ const baseId = Section.$generate_id(titleAsciiDoc, this)
661
+ this.getCatalog().refs['$[]='](baseId, true)
662
+ return `_${sep}${baseId}`
867
663
  }
868
664
 
869
665
  module.exports = produceAssemblyFile
@@ -4,14 +4,15 @@ 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')
7
8
  const selectMutableAttributes = require('./select-mutable-attributes')
8
9
 
9
10
  const ATTR_REF_RX = /\\?\{(\w[\w-]*)\}/g
10
- const IMAGE_MACRO_RX = /^image::?(.+?)\[(.*?)\]$/
11
11
 
12
- function produceAssemblyFiles (loadAsciiDoc, contentCatalog, assemblerConfig, resolveAssemblyModel) {
13
- const { asciidoc: assemblerAsciiDocConfig, assembly: assemblyConfig } = assemblerConfig
14
- resolveAssemblyModel ??= (componentVersion) => ({
12
+ function produceAssemblyFiles (loadAsciiDoc, contentCatalog, assemblerConfig, selectAssemblyProfile) {
13
+ const { assembly: assemblyConfig } = assemblerConfig
14
+ selectAssemblyProfile ??= (componentVersion) => ({
15
+ attributes: assemblyConfig.attributes,
15
16
  doctype: assemblyConfig.doctype,
16
17
  insertStartPage: assemblyConfig.insertStartPage,
17
18
  rootLevel: assemblyConfig.rootLevel,
@@ -21,36 +22,42 @@ function produceAssemblyFiles (loadAsciiDoc, contentCatalog, assemblerConfig, re
21
22
  embedReferenceStyle: assemblyConfig.embedReferenceStyle,
22
23
  linkReferenceStyle: assemblyConfig.linkReferenceStyle,
23
24
  dropExplicitXrefText: assemblyConfig.dropExplicitXrefText,
25
+ revdate: assemblyConfig.revdate,
24
26
  logger: assemblyConfig.logger, // for tests
25
27
  })
26
- const assemblerAsciiDocAttributes = Object.assign({}, assemblerAsciiDocConfig.attributes)
27
- const { revdate, 'source-highlighter': sourceHighlighter } = assemblerAsciiDocAttributes
28
- delete assemblerAsciiDocAttributes.revdate
29
- delete assemblerAsciiDocAttributes['source-highlighter']
28
+ const baseAssemblyAttributes = assemblyConfig.attributes
30
29
  const publishableFiles = contentCatalog.getFiles().filter((file) => file.out)
31
30
  let siteRoot
32
31
  const configMdc = assemblerConfig.file ? { file: { path: assemblerConfig.file } } : {}
33
32
  return filterComponentVersions(contentCatalog.getComponents(), assemblerConfig.componentVersionFilter.names).reduce(
34
33
  (accum, componentVersion) => {
35
- const assemblyModel = resolveAssemblyModel(componentVersion)
36
- if (!assemblyModel.navigation) return accum
34
+ const assemblyModel = selectAssemblyProfile(componentVersion)
35
+ const { attributes: assemblyAttributes, logger, navigation, rootLevel } = assemblyModel
36
+ if (!navigation) return accum
37
+ const contextualLogger = logger ? { warn: logger.warn.bind(logger, configMdc) } : undefined
37
38
  const { name: componentName, version, title } = componentVersion
38
39
  const componentVersionAsciiDocConfig = getAsciiDocConfigWithAsciidoctorReducerExtension(componentVersion)
40
+ let sourceHighlighter
41
+ if ('source-highlighter' in assemblyAttributes) {
42
+ sourceHighlighter = assemblyAttributes['source-highlighter']
43
+ delete assemblyAttributes['source-highlighter']
44
+ }
39
45
  const mergedAsciiDocAttributes = collateAsciiDocAttributes(
40
- Object.assign({ revdate }, componentVersionAsciiDocConfig.attributes),
41
- assemblerAsciiDocAttributes,
42
- { logger: assemblyModel.logger, mdc: configMdc }
46
+ Object.assign({ revdate: assemblyModel.revdate }, componentVersionAsciiDocConfig.attributes),
47
+ assemblyAttributes,
48
+ contextualLogger
43
49
  )
44
50
  const mergedAsciiDocConfig = Object.assign({}, componentVersionAsciiDocConfig, {
45
51
  attributes: mergedAsciiDocAttributes,
46
52
  })
47
- assemblyModel.outDirname = computeOut.call(contentCatalog, {
53
+ assemblyModel.filetype = baseAssemblyAttributes['assembler-filetype']
54
+ const outDirname = (assemblyModel.outDirname = computeOut.call(contentCatalog, {
48
55
  component: componentName,
49
56
  version,
50
57
  family: 'export',
51
58
  relative: '.index.adoc',
52
- }).dirname
53
- assemblyModel.filetype = assemblerAsciiDocAttributes['assembler-filetype']
59
+ }).dirname)
60
+ assemblyModel.pubRoot = outDirname ? '/' + outDirname : ''
54
61
  assemblyModel.siteRoot =
55
62
  siteRoot === undefined
56
63
  ? (siteRoot ??= ((val) => {
@@ -69,17 +76,22 @@ function produceAssemblyFiles (loadAsciiDoc, contentCatalog, assemblerConfig, re
69
76
  assemblyModel.linkReferenceStyle = 'absolute'
70
77
  }
71
78
  const auxiliaryImages = new Set()
79
+ const ctx = { component: componentName, version }
72
80
  Object.entries(mergedAsciiDocAttributes).forEach(([name, val]) => {
73
- const match = name.endsWith('-image') && val.startsWith('image:') && IMAGE_MACRO_RX.exec(val)
74
- if (!(match && isResourceRef(match[1]))) return
75
- // Q should we allow image to be resolved relative to component version?
76
- const image = contentCatalog.resolveResource(match[1], undefined, 'image', ['image'])
77
- if (!image?.out) return
78
- mergedAsciiDocAttributes[name] = `image:${image.out.path}[${match[2]}]`
79
- auxiliaryImages.add(image)
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
80
93
  })
81
94
  const rootEntry = { content: title }
82
- const { navigation, rootLevel } = assemblyModel
83
95
  let startPage =
84
96
  'startPage' in componentVersion
85
97
  ? componentVersion.startPage
@@ -123,9 +135,12 @@ function produceAssemblyFiles (loadAsciiDoc, contentCatalog, assemblerConfig, re
123
135
  accum.push(assemblyFile)
124
136
  return true
125
137
  }, false)
126
- sourceHighlighter
127
- ? (mergedAsciiDocAttributes['source-highlighter'] = sourceHighlighter)
128
- : delete mergedAsciiDocAttributes['source-highlighter']
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
+ }
129
144
  return accum
130
145
  },
131
146
  []
@@ -151,10 +166,6 @@ function includedInNav (items, url) {
151
166
  return items.find((it) => it.url === url || includedInNav(it.items || [], url))
152
167
  }
153
168
 
154
- function isResourceRef (target) {
155
- return ~target.indexOf(':') && !(~target.indexOf('://') || (target.startsWith('data:') && ~target.indexOf(',')))
156
- }
157
-
158
169
  function prepareOutlines (navigation, rootEntry, rootLevel) {
159
170
  const singleEntry = navigation.length === 1
160
171
  const items = navigation.reduce((accum, it) => {
@@ -178,7 +189,7 @@ function prepareOutlines (navigation, rootEntry, rootLevel) {
178
189
  return items
179
190
  }
180
191
 
181
- function collateAsciiDocAttributes (collated, additional, { logger, mdc }) {
192
+ function collateAsciiDocAttributes (collated, additional, logger) {
182
193
  Object.entries(additional).forEach(([name, val]) => {
183
194
  if (val && val.constructor === String) {
184
195
  let alias
@@ -189,9 +200,7 @@ function collateAsciiDocAttributes (collated, additional, { logger, mdc }) {
189
200
  if (refname in collated && ref === val) {
190
201
  alias = refval
191
202
  } else if (collated['attribute-missing'] === 'warn') {
192
- if (logger) {
193
- logger.warn(mdc, "Skipping reference to missing attribute '%s' in value of '%s' attribute", refname, name)
194
- }
203
+ logger?.warn("Skipping reference to missing attribute '%s' in value of '%s' attribute", refname, name)
195
204
  }
196
205
  return ref
197
206
  }
@@ -0,0 +1,7 @@
1
+ 'use strict'
2
+
3
+ function createResourceKey ({ component, version, module: mod, family, relative }) {
4
+ return `${version}@${component}:${mod === 'ROOT' ? '' : mod}:${family === 'page' ? '' : family + '$'}${relative}`
5
+ }
6
+
7
+ module.exports = createResourceKey
@@ -0,0 +1,25 @@
1
+ 'use strict'
2
+
3
+ const ReservedIdNames = 'content header footnotes footer footer-text premable toc toctitle'.split(' ')
4
+
5
+ function generateScopedId (componentSrc, componentVersion, coordinateSep, scopeSep, prefix, filetype, asXrefTarget) {
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 (filetype === 'html' && ReservedIdNames.includes(id)) {
17
+ id = prefix + id
18
+ prefix = ''
19
+ }
20
+ if (prefix && !/^[\p{Alpha}_:]/u.test(id)) id = prefix + id
21
+ const idLeader = id + scopeSep
22
+ return { idLeader, id }
23
+ }
24
+
25
+ module.exports = generateScopedId
@@ -0,0 +1,36 @@
1
+ 'use strict'
2
+
3
+ const path = require('node:path/posix')
4
+
5
+ function resolveEmbedTarget (resource, outDirname, referenceStyle, escapeForInline) {
6
+ const target =
7
+ referenceStyle === 'output-relative' ? resource.out.path : path.relative(outDirname + '/', resource.out.path)
8
+ return escapeForInline ? target.replace(/_/g, '{underscore}') : target
9
+ }
10
+
11
+ function resolveLinkTarget (resource, siteRoot, pubRoot, referenceStyle, escapeForInline, ensurePrefix = true) {
12
+ let target
13
+ if (resource.site?.url) {
14
+ target = ['', resource.pub.url]
15
+ } else {
16
+ switch (referenceStyle) {
17
+ case 'absolute':
18
+ target = ['', siteRoot.url + resource.pub.url]
19
+ break
20
+ case 'root-relative':
21
+ target = [ensurePrefix ? 'link:' : '', siteRoot.path + resource.pub.url]
22
+ break
23
+ default:
24
+ target = [ensurePrefix ? 'link:' : '', computeRelativeUrl(pubRoot + '/', resource.pub.url)]
25
+ }
26
+ }
27
+ if (escapeForInline) target[1] = target[1].replace(/_/g, '{underscore}')
28
+ return target.join('')
29
+ }
30
+
31
+ function computeRelativeUrl (from, to) {
32
+ const rel = path.relative(from, to)
33
+ return to.charAt(to.length - 1) === '/' ? rel + '/' : rel
34
+ }
35
+
36
+ module.exports = { resolveEmbedTarget, resolveLinkTarget }
@@ -0,0 +1,205 @@
1
+ 'use strict'
2
+
3
+ const createResourceKey = require('./create-resource-key')
4
+ const generateScopedId = require('./generate-scoped-id')
5
+ const parseResourceRef = require('./parse-resource-ref')
6
+ const { resolveEmbedTarget, resolveLinkTarget } = require('./resolver')
7
+ const { NamedIdAttrRx } = require('./rx')
8
+
9
+ function rewriteXrefs (
10
+ line,
11
+ contentCatalog,
12
+ assemblyModel,
13
+ ctx,
14
+ escapeForInline,
15
+ idCoordinateSep,
16
+ idScopeSep,
17
+ idPrefix,
18
+ idLeader,
19
+ pagesInOutline,
20
+ doc,
21
+ sourceLocation
22
+ ) {
23
+ return line.replace(/(?<![\\+])xref:((?:\.\/)?[\p{Alpha}0-9_/.{#].*?)\[(|.*?[^\\])\]/gu, (_, target, text) =>
24
+ rewriteXref(
25
+ target,
26
+ text,
27
+ contentCatalog,
28
+ assemblyModel,
29
+ ctx,
30
+ escapeForInline,
31
+ idCoordinateSep,
32
+ idScopeSep,
33
+ idPrefix,
34
+ idLeader,
35
+ pagesInOutline,
36
+ doc,
37
+ sourceLocation
38
+ )
39
+ )
40
+ }
41
+
42
+ function rewriteXref (
43
+ target,
44
+ text,
45
+ contentCatalog,
46
+ assemblyModel,
47
+ ctx,
48
+ escapeForInline,
49
+ idCoordinateSep,
50
+ idScopeSep,
51
+ idPrefix,
52
+ idLeader,
53
+ pagesInOutline,
54
+ doc,
55
+ sourceLocation
56
+ ) {
57
+ const rawTarget = target
58
+ if (doc && ~target.indexOf('{')) target = doc.$sub_attributes(target, global.Opal.hash({ attribute_missing: 'skip' }))
59
+ let fragment, resourceRef
60
+ const hashIdx = target.indexOf('#')
61
+ if (~hashIdx) {
62
+ resourceRef = target.slice(0, hashIdx)
63
+ fragment = target.slice(hashIdx + 1)
64
+ } else if (target.endsWith('.adoc') || ~target.indexOf('$')) {
65
+ resourceRef = target
66
+ fragment = ''
67
+ } else {
68
+ fragment = target
69
+ }
70
+ if (!resourceRef) return `xref:${idLeader || ''}${fragment}[${text}]`
71
+ const resourceId = parseResourceRef(resourceRef, ctx, 'page', contentCatalog)
72
+ const family = resourceId.family
73
+ let resource
74
+ if (family !== 'page' || !(resource = pagesInOutline?.get(createResourceKey(resourceId)))) {
75
+ if ((resource = contentCatalog.getById(resourceId))?.pub) {
76
+ const { linkReferenceStyle, pubRoot, siteRoot } = assemblyModel
77
+ text ||= resource.asciidoc?.xreftext || rawTarget
78
+ if (siteRoot || linkReferenceStyle === 'relative') {
79
+ return `${resolveLinkTarget(resource, siteRoot, pubRoot, linkReferenceStyle, escapeForInline)}${fragment && '#' + fragment}[${text}]`
80
+ }
81
+ if (doc) {
82
+ const msg = `Cannot create external ${family} reference in assembly because site URL is unknown: ${rawTarget}`
83
+ doc.logger.warn(doc.createLogMessage(msg, { source_location: sourceLocation }))
84
+ }
85
+ }
86
+ const linkAttrlist = text
87
+ ? (~text.indexOf(',') ? `"${text}"` : text) + ',role=unresolved'
88
+ : (~target.indexOf(':') ? `${rawTarget},` : '') + 'role=unresolved'
89
+ return `link:${rawTarget}[${linkAttrlist}]`
90
+ }
91
+ if (fragment === resource.asciidoc.id) fragment = ''
92
+ if (
93
+ text &&
94
+ (assemblyModel.dropExplicitXrefText === 'always' ||
95
+ (assemblyModel.dropExplicitXrefText === 'if-redundant' && text === resource.title))
96
+ ) {
97
+ text = ''
98
+ }
99
+ const componentVersionCtx = { name: ctx.component, version: ctx.version }
100
+ const { idLeader: refidLeader, id: refid } = generateScopedId(
101
+ resource.src,
102
+ componentVersionCtx,
103
+ idCoordinateSep,
104
+ idScopeSep,
105
+ idPrefix,
106
+ assemblyModel.filetype,
107
+ text.length > 0
108
+ )
109
+ return `xref:${fragment ? refidLeader + fragment : refid}[${text}]`
110
+ }
111
+
112
+ function rewriteImageAttr (val, contentCatalog, assemblyModel, ctx, assets) {
113
+ const match = val.startsWith('image:') && /^image::?(.+?)\[(.*?)\]$/.exec(val)
114
+ if (!match) return
115
+ const newTarget = rewriteImageRef(match[1], contentCatalog, assemblyModel, ctx, assets)
116
+ return newTarget ? `image:${newTarget}[${match[2]}]` : val
117
+ }
118
+
119
+ function rewriteInlineImages (line, contentCatalog, assemblyModel, ctx, assets, escapeForInline) {
120
+ return line.replace(/(?<![\\+])image:([^:\s[](?:[^[]*[^\s[])?)\[([^\]]*)\]/g, (m, target, attrlist) => {
121
+ const newTarget = rewriteImageRef(target, contentCatalog, assemblyModel, ctx, assets, escapeForInline)
122
+ return newTarget ? `image:${newTarget}[${attrlist}]` : m
123
+ })
124
+ }
125
+
126
+ function rewriteImageRef (target, contentCatalog, assemblyModel, ctx, assets, escapeForInline = false) {
127
+ const image = isResourceRef(target) && contentCatalog.resolveResource(target, ctx, 'image', ['image'])
128
+ if (!image?.out) return
129
+ let newTarget
130
+ const { filetype, embedReferenceStyle, linkReferenceStyle, outDirname, pubRoot, siteRoot } = assemblyModel
131
+ if (filetype !== 'html') {
132
+ newTarget = resolveEmbedTarget(image, outDirname, embedReferenceStyle, escapeForInline)
133
+ assets.add(image)
134
+ } else if (siteRoot || linkReferenceStyle === 'relative') {
135
+ newTarget = resolveLinkTarget(image, siteRoot, pubRoot, linkReferenceStyle, escapeForInline, false)
136
+ if (linkReferenceStyle === 'relative') assets.add(image)
137
+ }
138
+ if (!newTarget) return
139
+ return newTarget
140
+ }
141
+
142
+ function rewriteStyleAttribute (block, lines, idx, idLeader, replacementStyle = '') {
143
+ let prevLine = lines[idx - 1]
144
+ const char0 = prevLine?.charAt()
145
+ if (char0) {
146
+ if (
147
+ (char0 === '.' && /^\.\.?[^ \t.]/.test(prevLine)) ||
148
+ (char0 === '[' &&
149
+ prevLine.charAt(1) === '[' &&
150
+ /^\[\[(?:|[\p{Alpha}_:][\p{Alpha}0-9_\-:.]*(?:, *.+)?)\]\]$/u.test(prevLine))
151
+ ) {
152
+ return rewriteStyleAttribute(block, lines, idx - 1, idLeader, replacementStyle)
153
+ }
154
+ }
155
+ let cellSpec
156
+ if (
157
+ char0 &&
158
+ (char0 === '[' || (block.getDocument().isNested() && (cellSpec = prevLine.match(/^([^[|]*)\| *(\[.+)/)))) &&
159
+ prevLine.charAt(prevLine.length - 1) === ']'
160
+ ) {
161
+ if (cellSpec) {
162
+ prevLine = cellSpec[2]
163
+ cellSpec = cellSpec[1]
164
+ } else if (~prevLine.indexOf('id=')) {
165
+ prevLine = `[${prevLine.slice(1, -1).replace(NamedIdAttrRx, '')}]`
166
+ }
167
+ let rawStyle
168
+ const commaIdx = prevLine.indexOf(',')
169
+ if (~commaIdx) {
170
+ rawStyle = prevLine.slice(1, commaIdx)
171
+ if (~rawStyle.indexOf('=')) rawStyle = undefined
172
+ } else if (!~prevLine.indexOf('=')) {
173
+ rawStyle = prevLine.slice(1, prevLine.length - 1)
174
+ }
175
+ if (rawStyle) {
176
+ if (~rawStyle.indexOf('#')) {
177
+ prevLine = prevLine.replace(/#[^.%,\]]+/, `#${idLeader}${block.getId()}`)
178
+ if (replacementStyle) prevLine = prevLine.replace(/^[^#.%,\]]+/, `[${replacementStyle}`)
179
+ } else {
180
+ prevLine = `[${
181
+ replacementStyle ? rawStyle.replace(/^[^.%]*/, replacementStyle) : rawStyle
182
+ }#${idLeader}${block.getId()}${prevLine.slice(rawStyle.length + 1)}`
183
+ }
184
+ } else {
185
+ prevLine = `[${replacementStyle}#${idLeader}${block.getId()}${rawStyle == null ? ',' : ''}${prevLine.slice(1)}`
186
+ }
187
+ if (cellSpec) prevLine = `${cellSpec}|${prevLine}`
188
+ lines[idx - 1] = prevLine
189
+ } else {
190
+ lines.splice(idx, 0, `[${replacementStyle}#${idLeader}${block.getId()}]`)
191
+ }
192
+ }
193
+
194
+ function isResourceRef (str) {
195
+ return !(~str.indexOf(':') && (~str.indexOf('://') || (str.startsWith('data:') && ~str.indexOf(','))))
196
+ }
197
+
198
+ module.exports = {
199
+ rewriteXref,
200
+ rewriteXrefs,
201
+ rewriteImageAttr,
202
+ rewriteImageRef,
203
+ rewriteInlineImages,
204
+ rewriteStyleAttribute,
205
+ }
package/lib/util/rx.js ADDED
@@ -0,0 +1,5 @@
1
+ 'use strict'
2
+
3
+ const NamedIdAttrRx = /(?:^|, *)id=(?:("[^"]*"|'[^']*'|[^,]*))/
4
+
5
+ module.exports = { NamedIdAttrRx }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antora/assembler",
3
- "version": "1.0.0-beta.16",
3
+ "version": "1.0.0-beta.18",
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)",
@@ -30,7 +30,8 @@
30
30
  "./parse-resource-ref": "./lib/util/parse-resource-ref.js",
31
31
  "./produce-assembly-file": "./lib/produce-assembly-file.js",
32
32
  "./produce-assembly-files": "./lib/produce-assembly-files.js",
33
- "./select-mutable-attributes": "./lib/select-mutable-attributes.js"
33
+ "./select-mutable-attributes": "./lib/select-mutable-attributes.js",
34
+ "./package.json": "./package.json"
34
35
  },
35
36
  "imports": {
36
37
  "#asciidoctor-log-adapter": "./adapters/asciidoctor/jsonl-logger.rb",