@antora/assembler 1.0.0-beta.4 → 1.0.0-beta.6

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.
@@ -51,7 +51,7 @@ async function assembleContent (playbook, contentCatalog, converter, { configSou
51
51
  const { convert = converter, extname: targetExtname, mediaType: targetMediaType } = converter ?? {}
52
52
  if (!(convert && assemblyFiles.length)) return assemblyFiles
53
53
  const { publishSite: publishFiles = require('@antora/site-publisher') } = generatorFunctions
54
- await prepareWorkspace(publishFiles, assemblyFiles, contentCatalog, buildConfig)
54
+ await prepareWorkspace(publishFiles, assemblyFiles, buildConfig)
55
55
  const boundConvert = convert.bind(context)
56
56
  return new PromiseQueue({ concurrency: buildConfig.processLimit })
57
57
  .add(
@@ -100,22 +100,23 @@ async function assembleContent (playbook, contentCatalog, converter, { configSou
100
100
  }
101
101
 
102
102
  function createResolveAssemblyModel (context, contentCatalog, shared, intrinsicAttributes, navigationCatalog) {
103
+ const logger = context.getLogger ? context.getLogger(PACKAGE_NAME) : undefined
103
104
  const { assemblerProfiles } = context.getVariables()
104
105
  if (!assemblerProfiles) {
105
106
  return (componentVersion) => {
106
107
  const navigation =
107
108
  navigationCatalog?.getNavigation(componentVersion.name, componentVersion.version) ?? componentVersion.navigation
108
- return Object.assign({}, shared, { navigation })
109
+ return Object.assign({ logger }, shared, { navigation })
109
110
  }
110
111
  }
111
- const boundSendToLog = sendToLog.bind(context.getLogger ? context.getLogger(PACKAGE_NAME) : undefined)
112
+ const boundSendToLog = sendToLog.bind(logger)
112
113
  const { buildNavigation = require('@antora/navigation-builder') } = context.getFunctions()
113
114
  return (componentVersion) => {
114
115
  const componentVersionProfiles = assemblerProfiles.get(componentVersion.version + '@' + componentVersion.name)
115
116
  const overrides =
116
117
  componentVersionProfiles?.get(intrinsicAttributes['assembler-profile']) ?? componentVersionProfiles?.get() ?? {}
117
118
  const { navFiles, messages } = overrides
118
- const model = Object.assign({}, shared, overrides)
119
+ const model = Object.assign({ logger }, shared, overrides)
119
120
  delete model.navFiles
120
121
  delete model.messages
121
122
  const navigationOverride = navigationCatalog?.getNavigation(componentVersion.name, componentVersion.version)
@@ -177,7 +178,7 @@ function prepareConvertAttributes (doc, targetExtname, buildConfig) {
177
178
  } else {
178
179
  val = `!${name}${val === false ? '@' : ''}`
179
180
  }
180
- args.push('-a', val)
181
+ args.push(optionFlag, val)
181
182
  }
182
183
  return args
183
184
  },
@@ -206,7 +207,7 @@ function findFirstExportWithExtname (extname) {
206
207
  }
207
208
 
208
209
  // TODO: if no workspace dir is defined, we shouldn't continue
209
- function prepareWorkspace (publishFiles, assemblyFiles, contentCatalog, buildConfig) {
210
+ function prepareWorkspace (publishFiles, assemblyFiles, buildConfig) {
210
211
  const { dir, clean, keepSource } = buildConfig
211
212
  const files = []
212
213
  const outPaths = new Set()
@@ -223,7 +224,7 @@ function prepareWorkspace (publishFiles, assemblyFiles, contentCatalog, buildCon
223
224
 
224
225
  function isBound (obj) {
225
226
  if (obj == null) return false
226
- for (const p in obj) return true
227
+ for (const _ in obj) return true
227
228
  return false
228
229
  }
229
230
 
@@ -16,7 +16,11 @@ function loadConfig (playbook, configSource = './antora-assembler.yml') {
16
16
  () => false
17
17
  )
18
18
  .then((exists) =>
19
- exists ? fsp.readFile(configSource).then((data) => camelCaseKeys(yaml.load(data), ['asciidoc'])) : {}
19
+ exists
20
+ ? fsp
21
+ .readFile(configSource)
22
+ .then((data) => Object.assign(camelCaseKeys(yaml.load(data), ['asciidoc']), { file: configSource }))
23
+ : {}
20
24
  )
21
25
  ).then((config) => {
22
26
  if (config.enabled === false) return undefined
@@ -8,7 +8,7 @@ const unconvertInlineAsciiDoc = require('./util/unconvert-inline-asciidoc')
8
8
  const AttributeEntryRx = /^:([^:-][^:]*):(?: .*)?$/
9
9
  const BuiltInNamedEntities = { amp: '&', apos: "'", gt: '>', lt: '<', nbsp: ' ', quot: '"' }
10
10
  const CharRefRx = /&(?:([a-z][a-z]+\d{0,2})|#(?:(\d{2,6})|x([a-z\d]{2,5})));/g
11
- const DiscardAttributes = 'doctype leveloffset assembly-style underscore'.split(' ')
11
+ const DiscardAttributes = 'doctype leveloffset assembly-navtitle assembly-style underscore'.split(' ')
12
12
  const ReservedIdNames = 'content header footnotes footer footer-text premable toc toctitle'.split(' ')
13
13
 
14
14
  function produceAssemblyFile (
@@ -24,11 +24,10 @@ function produceAssemblyFile (
24
24
  const pagesByUrl = files.reduce((map, it) => (it.src.family === 'page' ? map.set(it.pub.url, it) : map), new Map())
25
25
  if (outline.urlType === 'internal' && !pagesByUrl.get(outline.url) && !(outline.items || []).length) return
26
26
  const pagesInOutline = selectPagesInOutline(outline, pagesByUrl, componentVersion)
27
- const navtitle = outline.content
28
27
  const buffer = mergeAsciiDoc(
29
28
  loadAsciiDoc,
30
29
  contentCatalog,
31
- buildAsciiDocHeader(componentVersion, navtitle, assemblyModel),
30
+ buildAsciiDocHeader(componentVersion, outline.content, assemblyModel),
32
31
  componentVersion,
33
32
  outline,
34
33
  files,
@@ -38,7 +37,7 @@ function produceAssemblyFile (
38
37
  assemblyModel
39
38
  )
40
39
  const rootLevel = assemblyModel.rootLevel
41
- const stem = rootLevel === 0 ? 'index' : generateSlug(navtitle)
40
+ const stem = rootLevel === 0 ? 'index' : generateSlug(buffer.navtitle)
42
41
  const downloadStem = [componentVersion.name, componentVersion.version, rootLevel === 0 ? '' : stem]
43
42
  .filter((it) => it)
44
43
  .join('-')
@@ -63,7 +62,7 @@ function buildAsciiDocHeader (componentVersion, navtitle, assemblyModel) {
63
62
  let doctitle = navtitleAsciiDoc
64
63
  if (navtitlePlain !== componentVersion.title) doctitle = `${componentVersion.title}: ${doctitle}`
65
64
  const version = componentVersion.version === 'master' ? '' : componentVersion.version
66
- return [
65
+ const buffer = [
67
66
  `= ${doctitle}`,
68
67
  ...(version ? [`:revnumber: ${version}`] : []),
69
68
  ...(doctype === 'article' ? [] : [`:doctype: ${doctype ?? 'book'}`]),
@@ -75,6 +74,7 @@ function buildAsciiDocHeader (componentVersion, navtitle, assemblyModel) {
75
74
  `:page-component-display-version: ${componentVersion.displayVersion}`,
76
75
  `:page-component-title: ${componentVersion.title}`,
77
76
  ]
77
+ return Object.assign(buffer, { navtitle })
78
78
  }
79
79
 
80
80
  function selectPagesInOutline (outlineEntry, pagesByUrl, componentVersion, accum) {
@@ -106,13 +106,19 @@ function mergeAsciiDoc (
106
106
  supportsParts = false
107
107
  ) {
108
108
  // TODO: we could try to be smart about it and make sure the page with fragment is included at least once
109
- if (outlineEntry.hash) return buffer
110
- const { content: navtitle, items = [], unresolved, urlType, url } = outlineEntry
109
+ if (outlineEntry.hash) {
110
+ buffer.inBody ??= false
111
+ return buffer
112
+ }
113
+ let navtitle = outlineEntry.content
114
+ let navtitlePlain = sanitize(navtitle)
115
+ let navtitleAsciiDoc = unconvertInlineAsciiDoc(navtitle)
116
+ const { items = [], unresolved, urlType, url } = outlineEntry
117
+ // FIXME: ideally, resource ID would be stored in navigation so we can look up the page more efficiently
118
+ const page = urlType === 'internal' && !unresolved ? pagesInOutline.get(url) : undefined
111
119
  const atDocumentRoot = !buffer.inBody
112
120
  const atBookRoot = atDocumentRoot && !level && assemblyModel.doctype === 'book' && (supportsParts = true)
113
121
  const hasItems = items.length > 0
114
- const navtitlePlain = sanitize(navtitle)
115
- const navtitleAsciiDoc = unconvertInlineAsciiDoc(navtitle)
116
122
  const siteUrl = ((val) => {
117
123
  if (!val || val === '/') return ''
118
124
  return val.charAt(val.length - 1) === '/' ? val.slice(0, val.length - 1) : val
@@ -120,25 +126,23 @@ function mergeAsciiDoc (
120
126
  const idSeparator = assemblyModel.xmlIds ? '-' : ':'
121
127
  const idScopeSeparator = idSeparator.repeat(3)
122
128
  const idCoordinateSeparator = idSeparator === '-' ? '----' : idSeparator
123
- // FIXME: ideally, resource ID would be stored in navigation so we can look up the page more efficiently
124
- let page = urlType === 'internal' && !unresolved ? pagesInOutline.get(url) : undefined
125
- if (page && pagesInOutline.assembled.pages.has(page)) page = undefined
126
- if (page) {
127
- let contents = page.src.contents
128
- if (contents == null) return buffer
129
- // NOTE: blank lines at top and bottom of document create mismatch when using line numbers to navigate source lines
130
- // IMPORTANT: this must not leave behind lines the parser will drop!
131
- // IDEA: another option is to capture initial lineno of reader and use as offset (but preseves those blank lines)
132
- contents = Buffer.from(
133
- contents
134
- .toString()
135
- .replace(/^(?:[ \t]*\r\n?|[ \t]*\n)+/, '')
136
- .trimRight()
137
- )
138
- const { component, version, module: module_, relative, origin } = page.src
129
+ if (page && !pagesInOutline.assembled.pages.has(page)) {
130
+ const contents = page.src.contents
131
+ if (contents == null) {
132
+ buffer.inBody ??= false
133
+ return buffer
134
+ }
135
+ const { component, version, module: module_, relative, origin, mediaType } = page.src
139
136
  const topicPrefix = ~relative.indexOf('/') ? path.dirname(relative) + '/' : ''
140
- const pageAsAsciiDoc = new page.constructor(Object.assign({}, page, { contents, mediaType: page.src.mediaType }))
137
+ const pageAsAsciiDoc = new page.constructor(
138
+ Object.assign({}, page, { contents: trimAsciiDoc(contents), mediaType })
139
+ )
141
140
  const doc = loadAsciiDoc(pageAsAsciiDoc, contentCatalog, asciidocConfig)
141
+ if (doc.hasAttribute('assembly-navtitle')) {
142
+ navtitleAsciiDoc = doc.getAttribute('assembly-navtitle')
143
+ navtitlePlain = sanitize((navtitle = doc.$apply_reftext_subs(navtitleAsciiDoc)))
144
+ if (buffer.inBody == null) buffer.navtitle = navtitle
145
+ }
142
146
  if (atDocumentRoot) {
143
147
  const authors = doc.getAuthors()
144
148
  if (authors.length) {
@@ -198,7 +202,6 @@ function mergeAsciiDoc (
198
202
  }
199
203
  buffer.push(`:page-module: ${module_}`)
200
204
  buffer.push(`:page-relative-src-path: ${relative}`)
201
- //buffer.push(`:page-origin-type: ${origin.type}`)
202
205
  buffer.push(`:page-origin-url: ${origin.url}`)
203
206
  buffer.push(`:page-origin-start-path:${origin.startPath && ' '}${origin.startPath}`)
204
207
  buffer.push(`:page-origin-refname: ${origin.branch || origin.tag}`)
@@ -400,9 +403,9 @@ function mergeAsciiDoc (
400
403
  if (~hashIdx && !relativePart.endsWith('.adoc')) relativePart += '.adoc'
401
404
  }
402
405
  if (!isPage || ~relativePart.indexOf('@') || /:.*:/.test(relativePart)) {
403
- if (siteUrl && (targetResource = contentCatalog.resolveResource(relativePart, page.src, 'page'))?.out) {
406
+ if (siteUrl && (targetResource = contentCatalog.resolveResource(relativePart, page.src, 'page'))?.pub) {
404
407
  text ||= targetResource.asciidoc?.xreftext || target
405
- return `${siteUrl}${targetResource.pub.url}${fragment && '#' + fragment}[${text}]`
408
+ return `${siteUrl}${targetResource.pub.url.replace(/_/g, '{underscore}')}${fragment && '#' + fragment}[${text}]`
406
409
  }
407
410
  // TODO: handle unresolved resource better
408
411
  return m
@@ -555,6 +558,7 @@ function mergeAsciiDoc (
555
558
  }
556
559
  } else if (level) {
557
560
  if (atDocumentRoot && navtitlePlain === componentVersion.title) {
561
+ buffer.inBody ??= false
558
562
  level--
559
563
  } else {
560
564
  buffer.inBody = true
@@ -663,6 +667,12 @@ function processDocumentHeader (doc, lines, buffer, ignoreLines) {
663
667
  } else if (belowDoctitle) {
664
668
  if (implicitLines.length === 2 || !/[\p{Alpha}0-9]/u.test(chr0)) break
665
669
  implicitLines.push(line)
670
+ } else if (chr0 === '[' && line.charAt(line.length - 1) === ']') {
671
+ const attrlist = line
672
+ .slice(1, -1)
673
+ .trim()
674
+ .replace(/(?:^\w[\w-]*(?!=|[\w-]))?([#.%]\w[\w-]*)*/, '')
675
+ if (attrlist) buffer.push('[' + attrlist + ']')
666
676
  }
667
677
  } else if (belowDoctitle) {
668
678
  break
@@ -685,7 +695,7 @@ function generateSlug (title) {
685
695
  return String.fromCharCode(dec ? parseInt(dec, 10) : parseInt(hex, 16))
686
696
  })
687
697
  .replace(/[\x27\u2019]/g, '')
688
- .replace(/[^\p{Alpha}0-9\-]/gu, '-')
698
+ .replace(/[^\p{Alpha}0-9-]/gu, '-')
689
699
  .replace(/^-+|-+$|(-)-+/g, '$1')
690
700
  }
691
701
 
@@ -752,7 +762,19 @@ function isResourceSpec (str) {
752
762
  }
753
763
 
754
764
  function getObjectId (obj) {
755
- return global.Opal.uid()
765
+ return global.Opal.id(obj)
766
+ }
767
+
768
+ // NOTE: blank lines at top and bottom of document create mismatch when using line numbers to navigate source lines
769
+ // IMPORTANT: this must not leave behind lines the parser will drop!
770
+ // IDEA: another option is to capture initial lineno of reader and use as offset (but preseves those blank lines)
771
+ function trimAsciiDoc (buffer) {
772
+ return Buffer.from(
773
+ buffer
774
+ .toString()
775
+ .replace(/^(?:[ \t]*\r\n?|[ \t]*\n)+/, '')
776
+ .trimRight()
777
+ )
756
778
  }
757
779
 
758
780
  function safePush (onto, entries) {
@@ -5,6 +5,7 @@ const filterComponentVersions = require('./filter-component-versions')
5
5
  const produceAssemblyFile = require('./produce-assembly-file')
6
6
  const selectMutableAttributes = require('./select-mutable-attributes')
7
7
 
8
+ const ATTR_REF_RX = /\\?\{(\w[\w-]*)\}/g
8
9
  const IMAGE_MACRO_RX = /^image::?(.+?)\[(.*?)\]$/
9
10
 
10
11
  function produceAssemblyFiles (loadAsciiDoc, contentCatalog, assemblerConfig, resolveAssemblyModel) {
@@ -22,16 +23,21 @@ function produceAssemblyFiles (loadAsciiDoc, contentCatalog, assemblerConfig, re
22
23
  delete assemblerAsciiDocAttributes.revdate
23
24
  delete assemblerAsciiDocAttributes['source-highlighter']
24
25
  const publishableFiles = contentCatalog.getFiles().filter((file) => file.out)
26
+ const configMdc = assemblerConfig.file ? { file: { path: assemblerConfig.file } } : {}
25
27
  return filterComponentVersions(contentCatalog.getComponents(), assemblerConfig.componentVersionFilter.names).reduce(
26
28
  (accum, componentVersion) => {
27
29
  const assemblyModel = resolveAssemblyModel(componentVersion)
28
30
  if (!assemblyModel.navigation) return accum
29
31
  const { name: componentName, version, title } = componentVersion
30
32
  const componentVersionAsciiDocConfig = getAsciiDocConfigWithAsciidoctorReducerExtension(componentVersion)
33
+ const mergedAsciiDocAttributes = collateAsciiDocAttributes(
34
+ Object.assign({ revdate }, componentVersionAsciiDocConfig.attributes),
35
+ assemblerAsciiDocAttributes,
36
+ { logger: assemblyModel.logger, mdc: configMdc }
37
+ )
31
38
  const mergedAsciiDocConfig = Object.assign({}, componentVersionAsciiDocConfig, {
32
- attributes: Object.assign({ revdate }, componentVersionAsciiDocConfig.attributes, assemblerAsciiDocAttributes),
39
+ attributes: mergedAsciiDocAttributes,
33
40
  })
34
- const mergedAsciiDocAttributes = mergedAsciiDocConfig.attributes
35
41
  const auxiliaryImages = new Set()
36
42
  Object.entries(mergedAsciiDocAttributes).forEach(([name, val]) => {
37
43
  const match = name.endsWith('-image') && val.startsWith('image:') && IMAGE_MACRO_RX.exec(val)
@@ -142,4 +148,33 @@ function prepareOutlines (navigation, rootEntry, rootLevel) {
142
148
  return items
143
149
  }
144
150
 
151
+ function collateAsciiDocAttributes (collated, additional, { logger, mdc }) {
152
+ Object.entries(additional).forEach(([name, val]) => {
153
+ if (val && val.constructor === String) {
154
+ let alias
155
+ val = val.replace(ATTR_REF_RX, (ref, refname) => {
156
+ if (ref.charAt() === '\\') return ref.substr(1)
157
+ const refval = collated[refname]
158
+ if (refval == null || refval === false) {
159
+ if (refname in collated && ref === val) {
160
+ alias = refval
161
+ } else if (collated['attribute-missing'] === 'warn') {
162
+ if (logger) {
163
+ logger.warn(mdc, "Skipping reference to missing attribute '%s' in value of '%s' attribute", refname, name)
164
+ }
165
+ }
166
+ return ref
167
+ }
168
+ if (refval.constructor === String) return refval
169
+ if (ref !== val) return refval.toString()
170
+ alias = refval
171
+ return ref
172
+ })
173
+ if (alias !== undefined) val = alias
174
+ }
175
+ collated[name] = val
176
+ })
177
+ return collated
178
+ }
179
+
145
180
  module.exports = produceAssemblyFiles
@@ -5,7 +5,7 @@ const { posix: path } = require('node:path')
5
5
  function computeOut (src) {
6
6
  const { component, version, module: module_, family, relative } = src
7
7
  const outRelative = family === 'page' ? relative.replace(/\.adoc$/, '.html') : relative
8
- const { dir: dirname, base: basename, ext: extname, name: stem } = path.parse(outRelative)
8
+ const { dir: dirname, base: basename } = path.parse(outRelative)
9
9
  const componentVersion = this.getComponentVersion(component, version)
10
10
  const versionSegment =
11
11
  'activeVersionSegment' in componentVersion
@@ -48,7 +48,7 @@ function resolveActiveVersionSegment (component, version) {
48
48
  this.removeFile((startPage = this.addFile({ src: startPageSrc })))
49
49
  }
50
50
  const outPathSegments = startPage.out.path.split('/')
51
- for (const depth of startPage.out.moduleRootPath.split('/')) outPathSegments.pop()
51
+ for (const _ of startPage.out.moduleRootPath.split('/')) outPathSegments.pop()
52
52
  if (startPageSrc.module !== 'ROOT') outPathSegments.pop()
53
53
  if (startPageSrc.component !== 'ROOT') outPathSegments.shift()
54
54
  return outPathSegments.length ? outPathSegments[0] : ''
@@ -1,7 +1,6 @@
1
1
  'use strict'
2
2
 
3
3
  const computeOut = require('./compute-out')
4
- const { posix: path } = require('node:path')
5
4
 
6
5
  function createAsciiDocFile (contentCatalog, file) {
7
6
  file.mediaType = 'text/asciidoc'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antora/assembler",
3
- "version": "1.0.0-beta.4",
3
+ "version": "1.0.0-beta.6",
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)",