@antora/assembler 1.0.0-rc.6 → 1.0.0-rc.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,20 +1,19 @@
1
1
  'use strict'
2
2
 
3
+ const collateAsciiDocAttributes = require('./util/collate-asciidoc-attributes')
3
4
  const filterComponentVersions = require('./filter-component-versions')
4
5
  const produceAssemblyFile = require('./produce-assembly-file')
5
- const selectMutableAttributes = require('./select-mutable-attributes')
6
-
7
- const ATTR_REF_RX = /\\?\{(\w[\w-]*)\}/g
6
+ const identifyMutableAttributes = require('./util/identify-mutable-attributes')
8
7
 
9
8
  function produceAssemblyFiles (
10
9
  loadAsciiDoc,
11
10
  contentCatalog,
12
11
  assemblerConfig,
13
- selectAssemblyProfile,
12
+ selectAssemblerProfile,
14
13
  selectComponentVersions
15
14
  ) {
16
15
  const { assembly: assemblyConfig } = assemblerConfig
17
- selectAssemblyProfile ??= (componentVersion) => ({
16
+ selectAssemblerProfile ??= (componentVersion) => ({
18
17
  attributes: assemblyConfig.attributes,
19
18
  doctype: assemblyConfig.doctype,
20
19
  insertStartPage: assemblyConfig.insertStartPage,
@@ -30,27 +29,28 @@ function produceAssemblyFiles (
30
29
  })
31
30
  const baseAssemblyAttributes = assemblyConfig.attributes
32
31
  const publishableFiles = contentCatalog.getFiles().filter((file) => file.out && file.pub)
32
+ const reducerExtension = require('@asciidoctor/reducer') // require lazily to wait for Opal to be defined
33
33
  let siteRoot
34
34
  const configMdc = assemblerConfig.file ? { file: { path: assemblerConfig.file } } : {}
35
35
  selectComponentVersions ??= filterComponentVersions.bind(null, assemblerConfig.componentVersionFilter)
36
36
  return selectComponentVersions(contentCatalog.getComponents()).reduce((accum, componentVersion) => {
37
- const assemblyModel = selectAssemblyProfile(componentVersion)
37
+ const assemblyModel = selectAssemblerProfile(componentVersion)
38
38
  const { attributes: assemblyAttributes, logger, navigation, rootLevel } = assemblyModel
39
39
  if (!navigation) return accum
40
40
  const contextualLogger = logger ? { warn: logger.warn.bind(logger, configMdc) } : undefined
41
41
  const { name: componentName, version, title } = componentVersion
42
- const componentVersionAsciiDocConfig = getAsciiDocConfigWithAsciidoctorReducerExtension(componentVersion)
42
+ const asciidocConfig = getAsciiDocConfigWithAsciidoctorReducerExtension(componentVersion, reducerExtension)
43
43
  let sourceHighlighter
44
44
  if ('source-highlighter' in assemblyAttributes) {
45
45
  sourceHighlighter = assemblyAttributes['source-highlighter']
46
46
  delete assemblyAttributes['source-highlighter']
47
47
  }
48
48
  const mergedAsciiDocAttributes = collateAsciiDocAttributes(
49
- Object.assign({}, componentVersionAsciiDocConfig.attributes),
49
+ Object.assign({}, asciidocConfig.attributes),
50
50
  assemblyAttributes,
51
51
  contextualLogger
52
52
  )
53
- const mergedAsciiDocConfig = Object.assign({}, componentVersionAsciiDocConfig, {
53
+ const mergedAsciiDocConfig = Object.assign({}, asciidocConfig, {
54
54
  attributes: mergedAsciiDocAttributes,
55
55
  })
56
56
  assemblyModel.filetype = baseAssemblyAttributes['assembler-filetype']
@@ -82,33 +82,10 @@ function produceAssemblyFiles (
82
82
  } else if (!(assemblyModel.filetype === 'pdf' && assemblyModel.linkReferenceStyle === 'relative')) {
83
83
  assemblyModel.linkReferenceStyle = 'absolute'
84
84
  }
85
- const rootEntry = { content: title }
86
- let startPage =
87
- 'startPage' in componentVersion
88
- ? componentVersion.startPage
89
- : contentCatalog.resolvePage('index.adoc', { component: componentName, version })
90
- if (startPage && startPage.src.component === componentName && startPage.src.version === version) {
91
- if (assemblyModel.insertStartPage) {
92
- const navtitle = startPage.asciidoc?.navtitle || rootEntry.content
93
- Object.assign(rootEntry, { navtitle, url: startPage.pub.url, urlType: 'internal', roles: ['page'] })
94
- }
95
- } else {
96
- // Q: should we always use a reference page as startPage for computing mutableAttributes?
97
- startPage = contentCatalog.createFile({
98
- src: {
99
- component: componentVersion.name,
100
- version: componentVersion.version,
101
- componentVersion,
102
- module: 'ROOT',
103
- family: 'page',
104
- relative: 'index.adoc',
105
- origin: (componentVersion.origins || [])[0],
106
- },
107
- })
108
- startPage.path = ['modules', startPage.src.module, startPage.src.family + 's', startPage.src.relative].join('/')
109
- }
110
- const mutableAttributes = selectMutableAttributes(loadAsciiDoc, contentCatalog, startPage, mergedAsciiDocConfig)
111
- prepareOutlines(navigation, rootEntry, rootLevel).reduce((any, outline) => {
85
+ const outlineRoot = { content: title }
86
+ const startPage = resolveStartPage(contentCatalog, componentVersion, assemblyModel.insertStartPage, outlineRoot)
87
+ const mutableAttributes = identifyMutableAttributes(loadAsciiDoc, contentCatalog, startPage, mergedAsciiDocConfig)
88
+ prepareOutlines(navigation, outlineRoot, rootLevel).reduce((any, outline) => {
112
89
  const assemblyFile = produceAssemblyFile(
113
90
  loadAsciiDoc,
114
91
  contentCatalog,
@@ -132,25 +109,54 @@ function produceAssemblyFiles (
132
109
  }, [])
133
110
  }
134
111
 
135
- function getAsciiDocConfigWithAsciidoctorReducerExtension (componentVersion) {
136
- const asciidoctorReducerExtension = require('@asciidoctor/reducer') // NOTE: must be required lazily
112
+ function getAsciiDocConfigWithAsciidoctorReducerExtension (componentVersion, reducerExtension) {
137
113
  const asciidocConfig = componentVersion.asciidoc
138
- const extensions = [asciidoctorReducerExtension]
114
+ const extensions = [reducerExtension]
139
115
  const configuredExtensions = asciidocConfig.extensions || []
140
- if (!configuredExtensions.length) return Object.assign({}, asciidocConfig, { extensions, sourcemap: true })
141
- return Object.assign({}, asciidocConfig, {
142
- extensions: configuredExtensions.reduce((accum, candidate) => {
143
- if (candidate !== asciidoctorReducerExtension) accum.push(candidate)
144
- return accum
145
- }, extensions),
146
- sourcemap: true,
147
- })
116
+ return configuredExtensions.length
117
+ ? Object.assign({}, asciidocConfig, {
118
+ extensions: configuredExtensions.reduce((accum, candidate) => {
119
+ if (candidate !== reducerExtension) accum.push(candidate)
120
+ return accum
121
+ }, extensions),
122
+ sourcemap: true,
123
+ })
124
+ : Object.assign({}, asciidocConfig, { extensions, sourcemap: true })
148
125
  }
149
126
 
150
127
  function includedInNav (items, url) {
151
128
  return items.find((it) => it.url === url || includedInNav(it.items || [], url))
152
129
  }
153
130
 
131
+ function resolveStartPage (contentCatalog, componentVersion, insertStartPage, outlineRoot) {
132
+ const { name: componentName, version } = componentVersion
133
+ let startPage =
134
+ 'startPage' in componentVersion
135
+ ? componentVersion.startPage
136
+ : contentCatalog.resolvePage('index.adoc', { component: componentName, version })
137
+ if (startPage && startPage.src.component === componentName && startPage.src.version === version) {
138
+ if (insertStartPage) {
139
+ const navtitle = startPage.asciidoc?.navtitle || outlineRoot.content
140
+ Object.assign(outlineRoot, { navtitle, url: startPage.pub.url, urlType: 'internal', roles: ['page'] })
141
+ }
142
+ } else {
143
+ startPage = contentCatalog.createFile({
144
+ src: {
145
+ component: componentName,
146
+ version,
147
+ componentVersion,
148
+ module: 'ROOT',
149
+ family: 'page',
150
+ relative: 'index.adoc',
151
+ origin: componentVersion.origins?.values().next().value,
152
+ },
153
+ })
154
+ // FIXME remove once upgrading to Antora 3.2.0
155
+ startPage.path = ['modules', startPage.src.module, startPage.src.family + 's', startPage.src.relative].join('/')
156
+ }
157
+ return startPage
158
+ }
159
+
154
160
  function prepareOutlines (navigation, rootEntry, rootLevel) {
155
161
  const singleEntry = navigation.length === 1
156
162
  const items = navigation.reduce((accum, it) => {
@@ -174,37 +180,9 @@ function prepareOutlines (navigation, rootEntry, rootLevel) {
174
180
  return items
175
181
  }
176
182
 
177
- function collateAsciiDocAttributes (collated, additional, logger) {
178
- Object.entries(additional).forEach(([name, val]) => {
179
- if (val && val.constructor === String) {
180
- let alias
181
- val = val.replace(ATTR_REF_RX, (ref, refname) => {
182
- if (ref.charAt() === '\\') return ref.substring(1)
183
- const refval = collated[refname]
184
- if (refval == null || refval === false) {
185
- if (refname in collated && ref === val) {
186
- alias = refval
187
- } else if (collated['attribute-missing'] === 'warn') {
188
- logger?.warn("Skipping reference to missing attribute '%s' in value of '%s' attribute", refname, name)
189
- }
190
- return ref
191
- }
192
- if (refval.constructor === String) return refval
193
- if (ref !== val) return refval.toString()
194
- alias = refval
195
- return ref
196
- })
197
- if (alias !== undefined) val = alias
198
- }
199
- collated[name] = val
200
- })
201
- return collated
202
- }
203
-
204
183
  function extractUrlPath (url) {
205
- if (!url) return ''
206
- const urlPath = new URL(url).pathname
207
- return urlPath === '/' ? '' : urlPath
184
+ const parsedUrl = url ? URL.parse(url) : null
185
+ return parsedUrl == null || parsedUrl.pathname === '/' ? '' : parsedUrl.pathname
208
186
  }
209
187
 
210
188
  module.exports = produceAssemblyFiles
@@ -0,0 +1,32 @@
1
+ 'use strict'
2
+
3
+ const ATTR_REF_RX = /\\?\{(\w[\w-]*)\}/g
4
+
5
+ function collateAsciiDocAttributes (collated, additional = {}, logger = undefined) {
6
+ Object.entries(additional).forEach(([name, val]) => {
7
+ if (val && val.constructor === String) {
8
+ let alias
9
+ val = val.replace(ATTR_REF_RX, (ref, refname) => {
10
+ if (ref.charAt() === '\\') return ref.substring(1)
11
+ const refval = collated[refname]
12
+ if (refval == null || refval === false) {
13
+ if (refname in collated && ref === val) {
14
+ alias = refval
15
+ } else if (collated['attribute-missing'] === 'warn') {
16
+ logger?.warn("Skipping reference to missing attribute '%s' in value of '%s' attribute", refname, name)
17
+ }
18
+ return ref
19
+ }
20
+ if (refval.constructor === String) return refval
21
+ if (ref !== val) return refval.toString()
22
+ alias = refval
23
+ return ref
24
+ })
25
+ if (alias !== undefined) val = alias
26
+ }
27
+ collated[name] = val
28
+ })
29
+ return collated
30
+ }
31
+
32
+ module.exports = collateAsciiDocAttributes
@@ -0,0 +1,18 @@
1
+ 'use strict'
2
+
3
+ function deepClone (o) {
4
+ switch (o.constructor) {
5
+ case Object:
6
+ return Object.keys(o).reduce((accum, k) => {
7
+ const v = o[k]
8
+ accum[k] = !v || typeof v !== 'object' ? v : deepClone(v)
9
+ return accum
10
+ }, {})
11
+ case Array:
12
+ return o.map((it) => (!it || typeof it !== 'object' ? it : deepClone(it)))
13
+ default:
14
+ return o
15
+ }
16
+ }
17
+
18
+ module.exports = deepClone
@@ -0,0 +1,25 @@
1
+ 'use strict'
2
+
3
+ /**
4
+ * Identifies AsciiDoc attributes that can be modified in the body of the page.
5
+ *
6
+ * This determination is made by loading a reference page with empty contents, retrieving the list of document attribute
7
+ * names, then removing any attributes that were passed via the API with some exceptions. The reason for this function
8
+ * is to be more prudent with which attributes to manage at the boundaries of an assembled page.
9
+ */
10
+ function identifyMutableAttributes (loadAsciiDoc, contentCatalog, referencePage, asciidocConfig) {
11
+ const doc = loadAsciiDoc(
12
+ new referencePage.constructor(
13
+ Object.assign({}, referencePage, { contents: Buffer.alloc(0), mediaType: 'text/asciidoc' })
14
+ ),
15
+ contentCatalog,
16
+ asciidocConfig
17
+ )
18
+ const immutableAttributeNames = doc.attribute_overrides.$keys()['$+'](['doctype'])
19
+ return Object.entries(doc.getAttributes()).reduce((accum, [name, val]) => {
20
+ if (!immutableAttributeNames.includes(name)) accum[name] = val
21
+ return accum
22
+ }, {})
23
+ }
24
+
25
+ module.exports = identifyMutableAttributes
@@ -1,20 +1,23 @@
1
1
  'use strict'
2
2
 
3
+ const fs = require('node:fs')
3
4
  const { PassThrough } = require('node:stream')
4
5
 
5
6
  // adapted from https://github.com/jpommerening/node-lazystream/blob/master/lib/lazystream.js | license: MIT
6
7
  class LazyReadable extends PassThrough {
7
- constructor (fn, options) {
8
- super(options)
8
+ constructor (fnOrPath) {
9
+ super()
10
+ const fn = typeof fnOrPath === 'function' ? fnOrPath : fs.createReadStream.bind(null, fnOrPath)
9
11
  this._read = function () {
10
12
  delete this._read // restores original method
11
- fn.call(this, options)
12
- .on('error', (err) => {
13
- let msg = 'Failed to create stream'
14
- if (err.syscall === 'open') msg += ` for path '${err.path}'`
15
- this.emit('error', new Error(msg, { cause: err }))
16
- })
17
- .pipe(this)
13
+ const readable = fn().on('error', (err) => {
14
+ let msg = 'Failed to create stream'
15
+ if (err.syscall === 'open') msg += ` for path '${err.path}'`
16
+ this.emit('error', new Error(msg, { cause: err }))
17
+ })
18
+ readable.pipe(this)
19
+ this.fd = readable.fd
20
+ this.path = readable.path
18
21
  return this._read.apply(this, arguments)
19
22
  }
20
23
  this.emit('readable')
@@ -4,6 +4,7 @@ const createResourceKey = require('./create-resource-key')
4
4
  const generateScopedId = require('./generate-scoped-id')
5
5
  const parseResourceRef = require('./parse-resource-ref')
6
6
  const { resolveEmbedTarget, resolveLinkTarget } = require('./resolver')
7
+ const toHash = require('./to-hash')
7
8
 
8
9
  const { NAMED_ID_ATTR_RX } = require('./rx')
9
10
 
@@ -50,7 +51,9 @@ function rewriteXref (
50
51
  sourceLocation
51
52
  ) {
52
53
  const rawTarget = target
53
- if (doc && ~target.indexOf('{')) target = doc.$sub_attributes(target, global.Opal.hash({ attribute_missing: 'skip' }))
54
+ if (sourceLocation && ~target.indexOf('{')) {
55
+ target = doc.$sub_attributes(target, toHash({ attribute_missing: 'skip' }))
56
+ }
54
57
  let fragment, resourceRef
55
58
  const hashIdx = target.indexOf('#')
56
59
  if (~hashIdx) {
@@ -72,21 +75,23 @@ function rewriteXref (
72
75
  }
73
76
  }
74
77
  if (family !== 'page' || !(resource = pagesInOutline?.get(createResourceKey(resourceId)))) {
78
+ let defaultText
75
79
  if ((resource = contentCatalog.getById(resourceId))?.pub) {
76
80
  const { linkReferenceStyle, pubRoot, siteRoot } = assemblyModel
77
- text ||= resource.asciidoc?.xreftext || rawTarget
81
+ defaultText = resource.asciidoc?.xreftext || rawTarget
78
82
  if (siteRoot || linkReferenceStyle === 'relative') {
79
83
  const hash = fragment && !(family === 'page' && fragment === resource.asciidoc.id) ? '#' + fragment : ''
80
- return `${resolveLinkTarget(resource, siteRoot, pubRoot, linkReferenceStyle, escapeForInline)}${hash}[${text}]`
84
+ const linkTarget = resolveLinkTarget(resource, siteRoot, pubRoot, linkReferenceStyle, escapeForInline)
85
+ return `${linkTarget}${hash}[${updateAttrlist(doc, text, defaultText)}]`
81
86
  }
82
- if (doc) {
87
+ if (sourceLocation) {
83
88
  const msg = `Cannot create external ${family} reference in assembly because site URL is unknown: ${rawTarget}`
84
89
  doc.logger.warn(doc.createLogMessage(msg, { source_location: sourceLocation }))
85
90
  }
91
+ } else if (~target.indexOf(':')) {
92
+ defaultText = rawTarget // use explicit text so AsciiDoc processor displays it correctly
86
93
  }
87
- const linkAttrlist = text
88
- ? (~text.indexOf(',') ? `"${text}"` : text) + ',role=unresolved'
89
- : (~target.indexOf(':') ? `${rawTarget},` : '') + 'role=unresolved'
94
+ const linkAttrlist = updateAttrlist(doc, text, defaultText, 'unresolved')
90
95
  return `link:${rawTarget.charAt() === ':' ? resourceId.module + rawTarget : rawTarget}[${linkAttrlist}]`
91
96
  }
92
97
  if (fragment === resource.asciidoc.id) fragment = ''
@@ -123,7 +128,7 @@ function rewriteInlineImages (line, contentCatalog, assemblyModel, ctx, assets,
123
128
  }
124
129
 
125
130
  function rewriteImageRef (target, contentCatalog, assemblyModel, ctx, assets, escapeForInline, doc) {
126
- if (doc && ~target.indexOf('{')) target = doc.$sub_attributes(target, global.Opal.hash({ attribute_missing: 'skip' }))
131
+ if (doc && ~target.indexOf('{')) target = doc.$sub_attributes(target, toHash({ attribute_missing: 'skip' }))
127
132
  const image = isResourceRef(target) && contentCatalog.resolveResource(target, ctx, 'image', ['image'])
128
133
  if (!image?.out) return
129
134
  let newTarget
@@ -135,7 +140,6 @@ function rewriteImageRef (target, contentCatalog, assemblyModel, ctx, assets, es
135
140
  newTarget = resolveLinkTarget(image, siteRoot, pubRoot, linkReferenceStyle, escapeForInline ?? false, false)
136
141
  if (linkReferenceStyle === 'relative') assets.add(image)
137
142
  }
138
- if (!newTarget) return
139
143
  return newTarget
140
144
  }
141
145
 
@@ -196,6 +200,27 @@ function isResourceRef (str) {
196
200
  return !(~str.indexOf(':') && (~str.indexOf('://') || (str.startsWith('data:') && ~str.indexOf(','))))
197
201
  }
198
202
 
203
+ function updateAttrlist (doc, attrlist, defaultText, role) {
204
+ const attrs = { named: {}, pos: [] }
205
+ if (attrlist) {
206
+ const normalize = ~attrlist.indexOf('\n')
207
+ if (normalize) attrlist = attrlist.replace(/\n/g, '\x1e')
208
+ doc
209
+ .$parse_attributes(attrlist)
210
+ .$entries()
211
+ .forEach(([k, v]) => {
212
+ v = v['$nil?']() ? '' : normalize ? v.replace(/\x1e/g, '\n') : v
213
+ typeof k === 'number' ? attrs.pos.push(v) : (attrs.named[k] = v)
214
+ })
215
+ }
216
+ if (defaultText) attrs.pos[0] ||= defaultText
217
+ if (role) attrs.named.role = (attrs.named.role ? attrs.named.role + ' ' : '') + role
218
+ const entries = []
219
+ for (const v of attrs.pos) entries.push(~v.indexOf(',') || ~v.indexOf('=') ? `"${v}"` : v)
220
+ for (const [k, v] of Object.entries(attrs.named)) entries.push(`${k}=` + (~v.indexOf(',') ? `"${v}"` : v))
221
+ return entries.join(',')
222
+ }
223
+
199
224
  module.exports = {
200
225
  rewriteXref,
201
226
  rewriteXrefs,
@@ -0,0 +1,7 @@
1
+ 'use strict'
2
+
3
+ function toHash (obj = {}) {
4
+ return global.Opal.hash(obj)
5
+ }
6
+
7
+ module.exports = toHash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antora/assembler",
3
- "version": "1.0.0-rc.6",
3
+ "version": "1.0.0-rc.7",
4
4
  "description": "A JavaScript library that merges AsciiDoc content from multiple pages in an Antora site into assembly files and delegates to an exporter to convert those files to another format, such as PDF.",
5
5
  "license": "MPL-2.0",
6
6
  "author": "OpenDevise Inc. (https://opendevise.com)",
@@ -27,14 +27,12 @@
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",
31
- "./produce-assembly-file": "./lib/produce-assembly-file.js",
32
- "./produce-assembly-files": "./lib/produce-assembly-files.js",
33
- "./select-mutable-attributes": "./lib/select-mutable-attributes.js",
34
30
  "./package.json": "./package.json"
35
31
  },
36
32
  "imports": {
37
33
  "#asciidoctor-log-adapter": "./adapters/asciidoctor/jsonl-logger.rb",
34
+ "#identify-mutable-attributes": "./lib/util/identify-mutable-attributes.js",
35
+ "#produce-assembly-files": "./lib/produce-assembly-files.js",
38
36
  "#unconvert-inline-asciidoc": "./lib/util/unconvert-inline-asciidoc.js"
39
37
  },
40
38
  "dependencies": {
@@ -1,27 +0,0 @@
1
- 'use strict'
2
-
3
- function selectMutableAttributes (loadAsciiDoc, contentCatalog, referencePage, asciidocConfig) {
4
- const doc = loadAsciiDoc(
5
- new referencePage.constructor(
6
- Object.assign({}, referencePage, { contents: Buffer.alloc(0), mediaType: 'text/asciidoc' })
7
- ),
8
- contentCatalog,
9
- asciidocConfig
10
- )
11
- const additionalMutableNames = [
12
- 'page-component-name',
13
- 'page-component-version',
14
- 'page-version',
15
- 'page-component-display-version',
16
- 'page-component-title',
17
- ]
18
- // we could consider using an Asciidoctor extension here to grab attributes passed via the API instead
19
- const immutableAttributeNames = doc.attribute_overrides.$keys()['$-'](additionalMutableNames)
20
- immutableAttributeNames.push('doctype')
21
- return Object.entries(doc.getAttributes()).reduce((accum, [name, val]) => {
22
- if (!immutableAttributeNames.includes(name)) accum[name] = val
23
- return accum
24
- }, {})
25
- }
26
-
27
- module.exports = selectMutableAttributes