@antora/assembler 1.0.0-beta.2 → 1.0.0-beta.20

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,15 +1,32 @@
1
1
  'use strict'
2
2
 
3
3
  const createAsciiDocFile = require('./util/create-asciidoc-file')
4
- 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 {
8
+ rewriteXrefs,
9
+ rewriteImageAttr,
10
+ rewriteImageRef,
11
+ rewriteInlineImages,
12
+ rewriteStyleAttribute,
13
+ } = require('./util/rewriter')
5
14
  const sanitize = require('./util/sanitize')
6
15
  const unconvertInlineAsciiDoc = require('./util/unconvert-inline-asciidoc')
7
16
 
8
- const AttributeEntryRx = /^:([^:-][^:]*):(?: .*)?$/
9
- const BuiltInNamedEntities = { amp: '&', apos: "'", gt: '>', lt: '<', nbsp: ' ', quot: '"' }
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(' ')
12
- const ReservedIdNames = 'content header footnotes footer footer-text premable toc toctitle'.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-slug',
26
+ 'assembly-style',
27
+ 'underscore',
28
+ ]
29
+ const { NAMED_ID_ATTR_RX } = require('./util/rx')
13
30
 
14
31
  function produceAssemblyFile (
15
32
  loadAsciiDoc,
@@ -23,39 +40,83 @@ function produceAssemblyFile (
23
40
  ) {
24
41
  const pagesByUrl = files.reduce((map, it) => (it.src.family === 'page' ? map.set(it.pub.url, it) : map), new Map())
25
42
  if (outline.urlType === 'internal' && !pagesByUrl.get(outline.url) && !(outline.items || []).length) return
26
- const pagesInOutline = selectPagesInOutline(outline, pagesByUrl, componentVersion)
27
- const navtitle = outline.content
43
+ const pagesInOutline = selectPagesInOutline(outline, pagesByUrl)
44
+ const { rootLevel, xmlIds } = assemblyModel
45
+ const idSeparators = {
46
+ prefix:
47
+ 'assembler-idprefix' in asciidocConfig.attributes ? (asciidocConfig.attributes['assembler-idprefix'] ?? '') : '_',
48
+ scope: xmlIds ? '---' : ':::',
49
+ coordinate: xmlIds ? '----' : ':',
50
+ generateIdFromTitle: generateIdFromTitle.bind(
51
+ loadAsciiDoc(
52
+ { contents: Buffer.alloc(0), src: { family: 'page', relative: 'generate-id-from-title.adoc' } },
53
+ undefined,
54
+ asciidocConfig
55
+ )
56
+ ),
57
+ }
58
+ const { name: component, version } = componentVersion
59
+ asciidocConfig = prepareAsciiDocConfig(
60
+ contentCatalog,
61
+ { component, version },
62
+ pagesInOutline,
63
+ asciidocConfig,
64
+ assemblyModel,
65
+ idSeparators
66
+ )
28
67
  const buffer = mergeAsciiDoc(
29
68
  loadAsciiDoc,
30
69
  contentCatalog,
31
- buildAsciiDocHeader(componentVersion, navtitle, assemblyModel),
70
+ buildAsciiDocHeader(componentVersion, outline.content, assemblyModel),
32
71
  componentVersion,
33
72
  outline,
34
73
  files,
35
74
  pagesInOutline,
75
+ idSeparators,
36
76
  asciidocConfig,
37
77
  mutableAttributes,
38
78
  assemblyModel
39
79
  )
40
- const rootLevel = assemblyModel.rootLevel
41
- const stem = rootLevel === 0 ? 'index' : generateSlug(navtitle)
42
- const downloadStem = [componentVersion.name, componentVersion.version, rootLevel === 0 ? '' : stem]
43
- .filter((it) => it)
44
- .join('-')
80
+ const stem = buffer.slug ? sanitizeSlug(buffer.slug) : generateSlug(rootLevel === 0 ? 'index' : buffer.navtitle)
81
+ const downloadStem = [component, version, stem === 'index' ? '' : stem].filter((it) => it).join('-')
45
82
  return createAsciiDocFile(contentCatalog, {
46
83
  asciidoc: asciidocConfig,
47
84
  assembler: { assembled: pagesInOutline.assembled, downloadStem, rootLevel },
48
85
  contents: Buffer.from(buffer.join('\n') + '\n'),
49
- src: {
50
- component: componentVersion.name,
51
- version: componentVersion.version,
52
- module: 'ROOT',
53
- family: 'export',
54
- relative: stem + '.adoc',
55
- },
86
+ src: { component, version, module: 'ROOT', family: 'export', relative: stem + '.adoc' },
56
87
  })
57
88
  }
58
89
 
90
+ function prepareAsciiDocConfig (contentCatalog, ctx, pagesInOutline, asciidocConfig, assemblyModel, idSeparators) {
91
+ let attributesModified
92
+ const configShared = asciidocConfig.$shared
93
+ const sharedAttributes = asciidocConfig.attributes
94
+ if (!configShared) {
95
+ if (configShared == null) {
96
+ const assets = pagesInOutline.assembled.assets
97
+ for (const [name, val] of Object.entries(sharedAttributes)) {
98
+ if (!(typeof val === 'string' && ~val.indexOf(':'))) continue
99
+ let newVal
100
+ if (!name.endsWith('-image') || !(newVal = rewriteImageAttr(val, contentCatalog, assemblyModel, ctx, assets))) {
101
+ if (~(newVal = val).indexOf('image:')) {
102
+ newVal = rewriteInlineImages(newVal, contentCatalog, assemblyModel, ctx, assets)
103
+ }
104
+ }
105
+ if (newVal !== val) sharedAttributes[name] = newVal
106
+ }
107
+ }
108
+ for (const [name, val] of Object.entries(sharedAttributes)) {
109
+ if (!(typeof val === 'string' && ~val.indexOf(':'))) continue
110
+ if (~val.indexOf('xref:')) {
111
+ const newVal = rewriteXrefs(val, contentCatalog, assemblyModel, ctx, false, pagesInOutline, idSeparators)
112
+ if (newVal !== val) (attributesModified ??= {})[name] = newVal
113
+ }
114
+ }
115
+ if (configShared == null) Object.defineProperty(asciidocConfig, '$shared', { value: attributesModified == null })
116
+ }
117
+ return Object.assign({}, asciidocConfig, { attributes: Object.assign({}, sharedAttributes, attributesModified) })
118
+ }
119
+
59
120
  function buildAsciiDocHeader (componentVersion, navtitle, assemblyModel) {
60
121
  const doctype = assemblyModel.doctype ?? 'book'
61
122
  const navtitlePlain = sanitize(navtitle)
@@ -63,30 +124,30 @@ function buildAsciiDocHeader (componentVersion, navtitle, assemblyModel) {
63
124
  let doctitle = navtitleAsciiDoc
64
125
  if (navtitlePlain !== componentVersion.title) doctitle = `${componentVersion.title}: ${doctitle}`
65
126
  const version = componentVersion.version === 'master' ? '' : componentVersion.version
66
- return [
127
+ const displayVersion = componentVersion.displayVersion
128
+ const buffer = [
67
129
  `= ${doctitle}`,
68
- ...(version ? [`:revnumber: ${version}`] : []),
130
+ ...(version ? [`:revnumber: ${displayVersion}`] : []),
69
131
  ...(doctype === 'article' ? [] : [`:doctype: ${doctype ?? 'book'}`]),
70
132
  ':underscore: _',
71
133
  // Q: should we pass these via the CLI so they cannot be modified?
72
134
  `:page-component-name: ${componentVersion.name}`,
73
135
  `:page-component-version:${version ? ' ' + version : ''}`,
74
136
  ':page-version: {page-component-version}',
75
- `:page-component-display-version: ${componentVersion.displayVersion}`,
137
+ `:page-component-display-version: ${displayVersion}`,
76
138
  `:page-component-title: ${componentVersion.title}`,
77
139
  ]
140
+ return Object.assign(buffer, { navtitle })
78
141
  }
79
142
 
80
- function selectPagesInOutline (outlineEntry, pagesByUrl, componentVersion, accum) {
143
+ function selectPagesInOutline (outlineEntry, pagesByUrl, accum) {
81
144
  accum ??= Object.assign(new Map(), { assembled: { pages: new Map(), assets: new Set() } })
82
145
  const page = outlineEntry.urlType === 'internal' ? pagesByUrl.get(outlineEntry.url) : undefined
83
146
  if (page) {
84
- if (page.src.component === componentVersion.name && page.src.version === componentVersion.version) {
85
- accum.set(`${page.src.module === 'ROOT' ? '' : page.src.module + ':'}${page.src.relative}`, page)
86
- }
87
- accum.set(page.pub.url, page)
147
+ accum.set(createResourceKey(page.src), page)
148
+ accum.set(outlineEntry.url, page)
88
149
  }
89
- for (const item of outlineEntry.items || []) selectPagesInOutline(item, pagesByUrl, componentVersion, accum)
150
+ for (const item of outlineEntry.items || []) selectPagesInOutline(item, pagesByUrl, accum)
90
151
  return accum
91
152
  }
92
153
 
@@ -98,6 +159,7 @@ function mergeAsciiDoc (
98
159
  outlineEntry,
99
160
  files,
100
161
  pagesInOutline,
162
+ idSeparators,
101
163
  asciidocConfig,
102
164
  mutableAttributes,
103
165
  assemblyModel,
@@ -106,68 +168,72 @@ function mergeAsciiDoc (
106
168
  supportsParts = false
107
169
  ) {
108
170
  // 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
171
+ if (outlineEntry.hash) {
172
+ buffer.inBody ??= false
173
+ return buffer
174
+ }
175
+ let navtitle = outlineEntry.content
176
+ let navtitlePlain = sanitize(navtitle)
177
+ let navtitleAsciiDoc = unconvertInlineAsciiDoc(navtitle)
178
+ const { items = [], unresolved, urlType, url } = outlineEntry
179
+ const { doctype, filetype, linkReferenceStyle, pubRoot, siteRoot, logger } = assemblyModel
180
+ const assembled = pagesInOutline.assembled
181
+ // FIXME: ideally, resource ID would be stored in navigation so we can look up the page more efficiently
182
+ const page = urlType === 'internal' && !unresolved ? pagesInOutline.get(url) : undefined
111
183
  const atDocumentRoot = !buffer.inBody
112
- const atBookRoot = atDocumentRoot && !level && assemblyModel.doctype === 'book' && (supportsParts = true)
184
+ const isRootPage = page && atDocumentRoot && !level
185
+ const atBookRoot = atDocumentRoot && !level && doctype === 'book' && (supportsParts = true)
113
186
  const hasItems = items.length > 0
114
- const navtitlePlain = sanitize(navtitle)
115
- const navtitleAsciiDoc = unconvertInlineAsciiDoc(navtitle)
116
- const siteUrl = ((val) => {
117
- if (!val || val === '/') return ''
118
- return val.charAt(val.length - 1) === '/' ? val.slice(0, val.length - 1) : val
119
- })(asciidocConfig.attributes['primary-site-url'] || asciidocConfig.attributes['site-url'])
120
- const idSeparator = assemblyModel.xmlIds ? '-' : ':'
121
- const idScopeSeparator = idSeparator.repeat(3)
122
- 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()
187
+ if (page && !assembled.pages.has(page)) {
188
+ const contents = page.src.contents
189
+ if (contents == null) {
190
+ buffer.inBody ??= false
191
+ return buffer
192
+ }
193
+ const { component, version, module: module_, relative, origin, mediaType } = page.src
194
+ const pageAsAsciiDoc = new page.constructor(
195
+ Object.assign({}, page, { contents: trimAsciiDoc(contents), mediaType })
137
196
  )
138
- const { component, version, module: module_, relative, origin } = page.src
139
- const topicPrefix = ~relative.indexOf('/') ? path.dirname(relative) + '/' : ''
140
- const pageAsAsciiDoc = new page.constructor(Object.assign({}, page, { contents, mediaType: page.src.mediaType }))
141
197
  const doc = loadAsciiDoc(pageAsAsciiDoc, contentCatalog, asciidocConfig)
142
- if (atDocumentRoot) {
143
- const authors = doc.getAuthors()
144
- if (authors.length) {
145
- const authorLine = authors
146
- .map((author) => {
147
- const email = author.getEmail()
148
- return email ? `${author.getName()} <${author.getEmail()}>` : author.getName()
149
- })
150
- .join('; ')
151
- buffer.splice(1, 0, authorLine)
198
+ doc.logger = logger
199
+ ? Object.assign(doc.getLogger().$dup(), {
200
+ delegate: {
201
+ warn () {
202
+ return logger.warn.apply(logger, arguments)
203
+ },
204
+ },
205
+ })
206
+ : doc.getLogger()
207
+ doc.source_header_attributes ??= doc.parent.$to_h()
208
+ if (isRootPage && doc.hasAttribute('assembly-slug')) buffer.slug = doc.getAttribute('assembly-slug')
209
+ if (doc.hasAttribute('assembly-navtitle')) {
210
+ navtitleAsciiDoc = doc.getAttribute('assembly-navtitle')
211
+ navtitlePlain = sanitize((navtitle = doc.$apply_reftext_subs(navtitleAsciiDoc)))
212
+ if (buffer.inBody == null) {
213
+ buffer.navtitle = navtitle
214
+ // Q do we need to assert !level here?
215
+ if (assemblyModel.rootLevel) {
216
+ let doctitle = navtitleAsciiDoc
217
+ if (navtitlePlain !== componentVersion.title) doctitle = `${componentVersion.title}: ${doctitle}`
218
+ buffer[0] = `= ${doctitle}`
219
+ }
152
220
  }
153
221
  }
154
222
  // NOTE: in Antora, docname is relative src path from module without file extension
155
223
  const docname = doc.getAttribute('docname')
156
- const docnameForId = docname.replace(/[/.]/g, '-')
157
- const qualifyId = component !== componentVersion.name
158
- let idScope = docnameForId
159
- let idPrefix
160
- if (qualifyId) {
161
- idScope = [component, module_ === 'ROOT' ? '' : module_, idScope].join(idCoordinateSeparator)
162
- } else if (module_ !== 'ROOT') {
163
- idScope = module_ + idCoordinateSeparator + idScope
164
- } else if (ReservedIdNames.includes(docnameForId)) {
165
- idScope = idPrefix = idScope + idScopeSeparator
166
- }
167
- idPrefix ??= idScope + idScopeSeparator
224
+ const pageId = generateScopedId(page.src, componentVersion, idSeparators, filetype)
225
+ const pageIdLeader = pageId + idSeparators.scope
168
226
  let pageFragment = ''
169
227
  let pageRoles = ''
170
228
  let pageStyle = doc.getAttribute('assembly-style', '')
229
+ if (
230
+ !pageStyle &&
231
+ atBookRoot &&
232
+ doc.isAttribute('preface-title') &&
233
+ !('assembly-style' in doc.source_header_attributes.$$smap)
234
+ ) {
235
+ pageStyle = 'preface'
236
+ }
171
237
  let part
172
238
  if ((part = pageStyle === 'part')) {
173
239
  pageStyle = ''
@@ -179,7 +245,10 @@ function mergeAsciiDoc (
179
245
  let nextSectionLevel = 1
180
246
  const lines = doc.getSourceLines()
181
247
  const ignoreLines = []
182
- buffer.inBody = true
248
+ if (!buffer.inBody) {
249
+ buffer.inBody = true
250
+ buffer.endHeaderIdx = buffer.length - 1
251
+ }
183
252
  buffer.push('')
184
253
  buffer.push(`:docname: ${docname}`)
185
254
  if (component !== lastComponentVersion.name) {
@@ -198,13 +267,68 @@ function mergeAsciiDoc (
198
267
  }
199
268
  buffer.push(`:page-module: ${module_}`)
200
269
  buffer.push(`:page-relative-src-path: ${relative}`)
201
- //buffer.push(`:page-origin-type: ${origin.type}`)
202
270
  buffer.push(`:page-origin-url: ${origin.url}`)
203
271
  buffer.push(`:page-origin-start-path:${origin.startPath && ' '}${origin.startPath}`)
204
272
  buffer.push(`:page-origin-refname: ${origin.branch || origin.tag}`)
205
273
  buffer.push(`:page-origin-reftype: ${origin.branch ? 'branch' : 'tag'}`)
206
274
  buffer.push(`:page-origin-refhash: ${origin.worktree ? '(worktree)' : origin.refhash}`)
207
- if (doc.hasHeader()) pageRoles = processDocumentHeader(doc, lines, buffer, ignoreLines)
275
+ if (doc.hasHeader()) {
276
+ for (const entry of processDocumentHeader(doc, lines, ignoreLines, isRootPage)) {
277
+ if (entry.type === 'author_line') {
278
+ buffer.splice(1, 0, entry.lines[0])
279
+ buffer.endHeaderIdx += 1
280
+ } else if (entry.type === 'attribute_entry') {
281
+ const name = entry.name
282
+ if (!entry.negated && !doc.isAttributeLocked(name)) {
283
+ let val, newVal
284
+ if (
285
+ name.endsWith('-image') &&
286
+ (val = doc.getAttribute(name)) &&
287
+ (newVal = rewriteImageAttr(val, contentCatalog, assemblyModel, page.src, assembled.assets))
288
+ ) {
289
+ if (newVal !== val) entry.lines = [`:${name}: ${newVal}`]
290
+ } else if (~(val = entry.lines.join('\n').slice(name.length + 3)).indexOf(':')) {
291
+ newVal = val
292
+ if (~newVal.indexOf('image:')) {
293
+ newVal = rewriteInlineImages(
294
+ newVal,
295
+ contentCatalog,
296
+ assemblyModel,
297
+ page.src,
298
+ assembled.assets,
299
+ false,
300
+ doc
301
+ )
302
+ }
303
+ if (~newVal.indexOf('xref:')) {
304
+ newVal = rewriteXrefs(
305
+ newVal,
306
+ contentCatalog,
307
+ assemblyModel,
308
+ page.src,
309
+ false,
310
+ pagesInOutline,
311
+ idSeparators,
312
+ pageIdLeader,
313
+ doc,
314
+ { file: relative, lineno: entry.lineno }
315
+ )
316
+ }
317
+ if (newVal !== val) entry.lines = `:${entry.name}: ${newVal}`.split('\n')
318
+ }
319
+ }
320
+ if (entry.promote) {
321
+ buffer.splice(buffer.endHeaderIdx + 1, 0, ...entry.lines)
322
+ buffer.endHeaderIdx += entry.lines.length
323
+ } else {
324
+ buffer.push(...entry.lines)
325
+ }
326
+ } else {
327
+ buffer.push(...entry.lines)
328
+ }
329
+ }
330
+ pageRoles = doc.getRoles().reduce((accum, role) => `${accum}.${role}`, '')
331
+ }
208
332
  let heading
209
333
  if (pageStyle && (part && level === 1 ? (level = 0) : level) === 0) {
210
334
  let htitleAsciiDoc = navtitleAsciiDoc
@@ -221,10 +345,10 @@ function mergeAsciiDoc (
221
345
  assemblyModel = Object.assign({}, assemblyModel, { sectionMergeStrategy: 'discrete' })
222
346
  } else {
223
347
  if (atDocumentRoot) {
224
- pageFragment = `#${idPrefix}${doc.getId() ?? pageStyle}`
225
- buffer.unshift(`[#${idScope}]`)
348
+ pageFragment = `#${pageIdLeader}${doc.getId() ?? pageStyle}`
349
+ buffer.unshift(`[#${pageId}]`)
226
350
  } else {
227
- pageFragment = `#${idScope}`
351
+ pageFragment = `#${pageId}`
228
352
  }
229
353
  heading = { title: htitleAsciiDoc, level: part ? 1 : 2 }
230
354
  nextSectionLevel++
@@ -233,7 +357,7 @@ function mergeAsciiDoc (
233
357
  if (atDocumentRoot && navtitlePlain === componentVersion.title) {
234
358
  level--
235
359
  } else {
236
- pageFragment = `#${idScope}`
360
+ pageFragment = `#${pageId}`
237
361
  if (part && level === 1) level--
238
362
  if ((heading = { title: navtitleAsciiDoc, level: level + 1 }).level > 6) {
239
363
  Object.assign(heading, { level: 6, style: `discrete.h${heading.level}` })
@@ -244,7 +368,7 @@ function mergeAsciiDoc (
244
368
  buffer.push(`[${heading.style ?? pageStyle}${pageFragment}${pageRoles}]`)
245
369
  buffer.push(`${'='.repeat(heading.level)} ${heading.title}`)
246
370
  } else if (atDocumentRoot) {
247
- buffer.unshift(`[#${idScope}]`)
371
+ buffer.unshift(`[#${pageId}]`)
248
372
  }
249
373
  let enclosed
250
374
  if (assemblyModel.sectionMergeStrategy === 'enclose' && hasItems && doc.hasSections()) {
@@ -254,28 +378,18 @@ function mergeAsciiDoc (
254
378
  //if (overviewTitle === navtitle) overviewTitle = doc.getAttribute('overview-title', 'Overview')
255
379
  const overviewTitle = doc.getAttribute('overview-title', 'Overview')
256
380
  buffer.push('')
257
- // NOTE: try to toggle sectids; otherwise, fallback to globally unique synthetic ID
258
- let toggleSectids, syntheticId
259
- if (doc.isAttribute('sectids')) {
260
- if (doc.isAttributeLocked('sectids')) {
261
- syntheticId = `__object-id-${getObjectId(outlineEntry)}`
262
- } else {
263
- buffer.push(':!sectids:')
264
- toggleSectids = true
265
- }
266
- }
381
+ const syntheticId = idSeparators.generateIdFromTitle(navtitleAsciiDoc, idSeparators)
267
382
  let hlevel = level + 2
268
383
  if (hlevel > 6) {
269
384
  const blockStyle = `discrete.h${hlevel}`
270
385
  hlevel = 6
271
- buffer.push(syntheticId ? `[${blockStyle}#${syntheticId}]` : `[${blockStyle}]`)
272
- } else if (syntheticId) {
386
+ buffer.push(`[${blockStyle}#${syntheticId}]`)
387
+ } else {
273
388
  buffer.push(`[#${syntheticId}]`)
274
389
  }
275
390
  buffer.push(`${'='.repeat(hlevel)} ${overviewTitle}`)
276
- if (toggleSectids) buffer.push(':sectids:')
277
391
  }
278
- pagesInOutline.assembled.pages.set(page, pageFragment)
392
+ assembled.pages.set(page, pageFragment)
279
393
  if (doc.hasSections()) {
280
394
  fixSectionLevels(doc.getSections(), atBookRoot && !pageStyle ? undefined : nextSectionLevel)
281
395
  }
@@ -349,7 +463,7 @@ function mergeAsciiDoc (
349
463
  if (
350
464
  line.charAt() === ':' &&
351
465
  ~line.indexOf(':', 2) &&
352
- (line.match(AttributeEntryRx) || ['', ''])[1].replace('!', '') === 'leveloffset'
466
+ (line.match(ATTR_ENTRY_RX) || ['', ''])[1].replace('!', '') === 'leveloffset'
353
467
  ) {
354
468
  if (lines[idx - 1] === '') lines[idx - 1] = undefined
355
469
  lines[idx] = undefined
@@ -361,96 +475,31 @@ function mergeAsciiDoc (
361
475
  if (!refs['$key?'](refid) && (~refid.indexOf(' ') || refid.toLowerCase() !== refid)) {
362
476
  if ((refid = doc.$resolve_id(refid))['$nil?']()) return m
363
477
  }
364
- return `<<${idPrefix}${refid}${text ? ',' + text : ''}>>`
478
+ return `<<${pageIdLeader}${refid}${text ? ',' + text : ''}>>`
365
479
  })
366
480
  }
367
481
  // NOTE: the next check takes care of inline and block anchors
368
482
  if (~line.indexOf('[[')) {
369
- line = line.replace(/\[\[([\p{Alpha}_:][\p{Alpha}0-9_\-:.]*)(|, *.+?)\]\]/gu, `[[${idPrefix}$1$2]]`)
370
- }
371
- if (~line.indexOf('xref:')) {
372
- // Q: should we allow : as first character of target?
373
- line = line.replace(/(?<![\\+])xref:((?:\.\/)?[\p{Alpha}0-9_/.{#].*?)\[(|.*?[^\\])\]/gu, (m, target, text) => {
374
- let pagePart, fragment, targetPage
375
- const hashIdx = target.indexOf('#')
376
- if (~hashIdx) {
377
- pagePart = target.slice(0, hashIdx)
378
- fragment = target.slice(hashIdx + 1)
379
- // TODO: for now, assume .adoc; in the future, consider other file extensions
380
- if (pagePart && !pagePart.endsWith('.adoc')) pagePart += '.adoc'
381
- } else if (target.endsWith('.adoc')) {
382
- pagePart = target
383
- fragment = ''
384
- } else {
385
- fragment = target
386
- }
387
- if (!pagePart) {
388
- // Q: should we validate the internal ID here?
389
- return text && ~text.indexOf('=')
390
- ? `xref:${idPrefix}${fragment}[${text}]`
391
- : `<<${idPrefix}${fragment}${text ? ',' + text.replace(/\\]/g, ']') : ''}>>`
392
- }
393
- if (~pagePart.indexOf('@') || /:.*:/.test(pagePart)) {
394
- if (siteUrl && (targetPage = contentCatalog.resolvePage(pagePart, page.src)) && targetPage.out) {
395
- text ||= targetPage.asciidoc?.xreftext || target
396
- return `${siteUrl}${targetPage.pub.url}${fragment && '#' + fragment}[${text}]`
397
- }
398
- // TODO: handle unresolved page better
399
- return m
400
- }
401
- let targetModule
402
- const colonIdx = pagePart.indexOf(':')
403
- if (~colonIdx) {
404
- targetModule = pagePart.slice(0, colonIdx)
405
- pagePart = pagePart.slice(colonIdx + 1)
406
- } else {
407
- targetModule = module_
408
- }
409
- if (pagePart.startsWith('./')) pagePart = topicPrefix + pagePart.slice(2)
410
- const pageResourceRef = targetModule === 'ROOT' ? pagePart : `${targetModule}:${pagePart}`
411
- if (!(targetPage = pagesInOutline.get(pageResourceRef))) {
412
- if (siteUrl && (targetPage = contentCatalog.resolvePage(pageResourceRef, page.src)) && targetPage.out) {
413
- text ||= targetPage.asciidoc?.xreftext || target
414
- return `${siteUrl}${targetPage.pub.url}${fragment && '#' + fragment}[${text}]`
415
- }
416
- // TODO: handle unresolved page better
417
- return m
418
- }
419
- if (targetModule !== 'ROOT') pagePart = `${targetModule}${idCoordinateSeparator}${pagePart}`
420
- pagePart = pagePart.replace(/\.adoc$/, '').replace(/[/.]/g, '-')
421
- const refid = fragment
422
- ? `${pagePart}${idScopeSeparator}${fragment}`
423
- : pagePart + (ReservedIdNames.includes(pagePart) ? idScopeSeparator : '')
424
- return `<<${refid}${text && text !== targetPage.title ? ',' + text.replace(/\\]/g, ']') : ''}>>`
483
+ line = line.replace(/\[\[([\p{Alpha}_:][\p{Alpha}0-9_\-:.]*)(|, *.+?)\]\]/gu, (_, refid, text) => {
484
+ return `[[${pageIdLeader}${refid}${text}]]`
425
485
  })
426
486
  }
427
- if (~line.indexOf('link:{attachmentsdir}/')) {
428
- line = line.replace(/(?<![\\+])link:\{attachmentsdir\}\/([^\s[]+)\[(|.*?[^\\])\]/g, (m, relative, text) => {
429
- const attachment =
430
- siteUrl &&
431
- contentCatalog.getById({
432
- component: componentVersion.name,
433
- version: componentVersion.version,
434
- module: module_,
435
- family: 'attachment',
436
- relative,
437
- })
438
- // TODO: handle unresolved attachment page
439
- return attachment?.out ? `${siteUrl}${attachment.pub.url.replace(/_/g, '{underscore}')}[${text}]` : m
440
- })
487
+ if (~line.indexOf('xref:')) {
488
+ line = rewriteXrefs(
489
+ line,
490
+ contentCatalog,
491
+ assemblyModel,
492
+ page.src,
493
+ true,
494
+ pagesInOutline,
495
+ idSeparators,
496
+ pageIdLeader,
497
+ doc,
498
+ { file: relative, lineno: idx + 1 }
499
+ )
441
500
  }
442
501
  if (~line.indexOf('image:') && !line.startsWith('image::')) {
443
- line = line.replace(/(?<![\\+])image:([^:\s[](?:[^[]*[^\s[])?)\[([^\]]*)\]/g, (m, target, attrlist) => {
444
- if (isResourceSpec(target)) {
445
- const image = contentCatalog.resolveResource(target, page.src, 'image', ['image'])
446
- // TODO: handle (or report) unresolved image better
447
- if (image?.out) {
448
- pagesInOutline.assembled.assets.add(image)
449
- return `image:${image.out.path.replace(/_/g, '{underscore}')}[${attrlist}]`
450
- }
451
- }
452
- return m
453
- })
502
+ line = rewriteInlineImages(line, contentCatalog, assemblyModel, page.src, assembled.assets, true, doc)
454
503
  }
455
504
  lines[idx] = line
456
505
  }
@@ -476,7 +525,7 @@ function mergeAsciiDoc (
476
525
  return '='.repeat(targetMarkerLength) + ' ' + rest
477
526
  })
478
527
  // NOTE: ID will be undefined if sectids are turned off
479
- if (block.getId()) rewriteStyleAttribute(block, lines, idx, idPrefix, blockStyle)
528
+ if (block.getId()) rewriteStyleAttribute(block, lines, idx, pageIdLeader, blockStyle)
480
529
  } else {
481
530
  if (context === 'image') {
482
531
  let line = lines[idx] || ''
@@ -495,31 +544,24 @@ function mergeAsciiDoc (
495
544
  imageMacroOffset = -1
496
545
  }
497
546
  }
498
- if (imageMacroOffset >= 0) {
547
+ if (~imageMacroOffset) {
499
548
  const target = block.getAttribute('target')
500
- if (isResourceSpec(target)) {
501
- const image = contentCatalog.resolveResource(target, page.src, 'image', ['image'])
502
- // FIXME: handle (or report) case when image is not resolved
503
- if (image?.out) {
504
- const boxedAttrlist = line.slice(line.indexOf('['))
505
- pagesInOutline.assembled.assets.add(image)
506
- lines[idx] = `${prefix}image::${image.out.path}${boxedAttrlist}`
507
- }
508
- }
549
+ const newTarget = rewriteImageRef(target, contentCatalog, assemblyModel, page.src, assembled.assets)
550
+ if (newTarget) lines[idx] = `${prefix}image::${newTarget}${line.slice(line.indexOf('['))}`
509
551
  lastImageMacroAt = [idx, imageMacroOffset]
510
552
  }
511
553
  } else if (context === 'document' && block.hasHeader()) {
512
554
  // nested document
513
555
  idx = (block.getHeader().getLineNumber() || idx + 1) - 1
514
556
  }
515
- if (block.getId()) rewriteStyleAttribute(block, lines, idx, idPrefix)
557
+ if (block.getId()) rewriteStyleAttribute(block, lines, idx, pageIdLeader)
516
558
  }
517
559
  })
518
560
  safePush(
519
561
  buffer,
520
562
  lines.filter((it) => it !== undefined)
521
563
  )
522
- const attributeEntries = Object.entries(doc.source_header_attributes?.$$smap || {})
564
+ const attributeEntries = Object.entries(doc.source_header_attributes.$$smap)
523
565
  if (attributeEntries.length) {
524
566
  const resolvedAttributeEntries = attributeEntries.reduce(
525
567
  (accum, [name, val]) => {
@@ -531,7 +573,7 @@ function mergeAsciiDoc (
531
573
  } else if (val !== initialVal) {
532
574
  accum.push(`:${name}:${initialVal ? ' ' + initialVal : ''}`)
533
575
  }
534
- } else if (!(val == null || doc.isAttributeLocked(name) || DiscardAttributes.includes(name))) {
576
+ } else if (!(val == null || doc.isAttributeLocked(name) || DISCARD_ATTRIBUTE_NAMES.includes(name))) {
535
577
  accum.push(`:!${name}:`)
536
578
  }
537
579
  return accum
@@ -542,43 +584,36 @@ function mergeAsciiDoc (
542
584
  }
543
585
  } else if (level) {
544
586
  if (atDocumentRoot && navtitlePlain === componentVersion.title) {
587
+ buffer.inBody ??= false
545
588
  level--
546
589
  } else {
547
590
  buffer.inBody = true
548
591
  buffer.push('')
549
- // NOTE: try to toggle sectids; otherwise, fallback to globally unique synthetic ID
550
- // Q: should we unset docname, page-module, etc?
551
- let toggleSectids, syntheticId
552
- if (!('sectids' in asciidocConfig.attributes)) {
553
- buffer.push(':!sectids:')
554
- toggleSectids = true
555
- } else if (typeof asciidocConfig.attributes.sectids === 'string') {
556
- if ('sectids' in mutableAttributes) {
557
- buffer.push(':!sectids:')
558
- toggleSectids = true
559
- } else {
560
- syntheticId = `__object-id-${global.Opal.hash(outlineEntry).$object_id()}`
561
- }
562
- }
592
+ const syntheticId = idSeparators.generateIdFromTitle(navtitleAsciiDoc, idSeparators)
563
593
  let sectionTitle = navtitleAsciiDoc
564
594
  if (urlType === 'external') {
565
595
  sectionTitle = `${url}[${navtitleAsciiDoc.replace(/\]/g, '\\]')}]`
566
- } else if (urlType === 'internal' && !unresolved && siteUrl) {
596
+ } else if (urlType === 'internal' && !unresolved) {
567
597
  const resource = files.find((it) => it.pub.url === url)
568
- if (resource) sectionTitle = `${siteUrl}${resource.pub.url}[${navtitleAsciiDoc.replace(/\]/g, '\\]')}]`
598
+ if (resource) {
599
+ if (resource.src.family === 'page' && pagesInOutline.has(resource.pub.url)) {
600
+ const refid = generateScopedId(resource.src, componentVersion, idSeparators, filetype, true)
601
+ sectionTitle = `xref:${refid}[${navtitleAsciiDoc.replace(/\]/g, '\\]')}]`
602
+ } else if (siteRoot) {
603
+ sectionTitle = `${resolveLinkTarget(resource, siteRoot, pubRoot, linkReferenceStyle, true)}[${navtitleAsciiDoc.replace(/\]/g, '\\]')}]`
604
+ }
605
+ }
569
606
  }
570
607
  let hlevel = level + 1
571
608
  if (hlevel > 6) {
572
609
  hlevel = 6
573
- buffer.push(syntheticId ? `[discrete#${syntheticId}]` : '[discrete]')
574
- } else if (syntheticId) {
610
+ buffer.push(`[discrete#${syntheticId}]`)
611
+ } else {
575
612
  buffer.push(`[#${syntheticId}]`)
576
613
  }
577
614
  buffer.push(`${'='.repeat(hlevel)} ${sectionTitle}`)
578
- if (toggleSectids) buffer.push(':sectids:')
579
615
  }
580
616
  }
581
-
582
617
  if (hasItems) {
583
618
  const nextLevel = level + 1
584
619
  // NOTE: drop first child if same as parent; should we keep if content is different?
@@ -594,6 +629,7 @@ function mergeAsciiDoc (
594
629
  item,
595
630
  files,
596
631
  pagesInOutline,
632
+ idSeparators,
597
633
  asciidocConfig,
598
634
  mutableAttributes,
599
635
  assemblyModel,
@@ -606,11 +642,32 @@ function mergeAsciiDoc (
606
642
  return buffer
607
643
  }
608
644
 
609
- function processDocumentHeader (doc, lines, buffer, ignoreLines) {
645
+ function processDocumentHeader (doc, lines, ignoreLines, isRootPage) {
646
+ const entries = []
647
+ let assemblyHeaderAttributes = isRootPage && doc.getAttribute('assembly-header-attributes', '%authors')
648
+ assemblyHeaderAttributes = new Set(assemblyHeaderAttributes ? assemblyHeaderAttributes.split(/, */) : undefined)
649
+ if (assemblyHeaderAttributes.size) {
650
+ if (assemblyHeaderAttributes.has('%authors')) {
651
+ const authors = doc.getAuthors()
652
+ if (authors.length) {
653
+ entries.push({
654
+ type: 'author_line',
655
+ promote: true,
656
+ lines: [
657
+ authors
658
+ .map((author) => (author.getEmail() ? `${author.getName()} <${author.getEmail()}>` : author.getName()))
659
+ .join('; '),
660
+ ],
661
+ })
662
+ }
663
+ assemblyHeaderAttributes.delete('%authors')
664
+ }
665
+ const headerAttributes = doc.source_header_attributes
666
+ for (const name of assemblyHeaderAttributes) headerAttributes.$delete(name)
667
+ }
610
668
  const doctitleIdx = doc.getHeader().getLineNumber() - 1
611
669
  const end = doc.getBlocks()[0]?.getLineNumber() ?? lines.length
612
- let belowDoctitle
613
- let open
670
+ let belowDoctitle, current, open
614
671
  const implicitLines = []
615
672
  for (let idx = 0; idx < end; idx++) {
616
673
  if (idx === doctitleIdx) {
@@ -621,35 +678,51 @@ function processDocumentHeader (doc, lines, buffer, ignoreLines) {
621
678
  }
622
679
  const line = lines[idx]
623
680
  if (open === ':' || open === '-:') {
624
- if (line) {
625
- if (open === ':') buffer.push(line)
626
- if (!line.endsWith(' \\')) open = undefined
627
- } else {
628
- open = undefined
629
- }
681
+ if (open === ':') current.lines.push(line)
682
+ if (!line || !line.endsWith(' \\')) current = open = undefined
630
683
  } else if (line) {
631
- const chr0 = line.charAt()
632
684
  let attributeEntryMatch
685
+ const chr0 = line.charAt()
633
686
  if (chr0 === '/' && line.charAt(1) === '/') {
634
- if (line.startsWith('////')) {
635
- open = open ? (open === line ? undefined : open) : line
636
- } else if (belowDoctitle && !open && line.charAt(2) === '/') {
687
+ if (line.startsWith('////') && line === '/'.repeat(line.length)) {
688
+ if (open) {
689
+ current.lines.push(line)
690
+ if (open === line) current = open = undefined
691
+ } else {
692
+ entries.push((current = { type: 'block_comment', lines: [(open = line)] }))
693
+ }
694
+ } else if (open) {
695
+ current.lines.push(line)
696
+ } else if (belowDoctitle && line.charAt(2) === '/') {
637
697
  break
698
+ } else {
699
+ entries.push({ type: 'line_comment', lines: [line] })
700
+ current = undefined
638
701
  }
639
- buffer.push(line)
640
702
  } else if (open) {
641
- buffer.push(line)
642
- } else if (chr0 === ':' && ~line.indexOf(':', 2) && (attributeEntryMatch = line.match(AttributeEntryRx))) {
643
- const attributeName = attributeEntryMatch[1].replace('!', '')
644
- if (DiscardAttributes.includes(attributeName)) {
645
- if (line.endsWith(' \\')) open = '-:'
703
+ current.lines.push(line)
704
+ } else if (chr0 === ':' && ~line.indexOf(':', 2) && (attributeEntryMatch = line.match(ATTR_ENTRY_RX))) {
705
+ let name = attributeEntryMatch[1]
706
+ const negated = name.charAt() === '!' || name.charAt(name.length - 1) === '!'
707
+ if (negated) name = name.replace('!', '')
708
+ if (DISCARD_ATTRIBUTE_NAMES.includes(name)) {
709
+ if (line.endsWith(' \\')) open = '-:' // disallow value continuation
646
710
  } else {
647
711
  if (line.endsWith(' \\')) open = ':'
648
- buffer.push(line)
712
+ entries.push((current = { type: 'attribute_entry', name, negated, lines: [line], lineno: idx + 1 }))
713
+ if (assemblyHeaderAttributes.has(name)) current.promote = true
649
714
  }
650
715
  } else if (belowDoctitle) {
651
716
  if (implicitLines.length === 2 || !/[\p{Alpha}0-9]/u.test(chr0)) break
652
717
  implicitLines.push(line)
718
+ } else if (chr0 === '[' && line.charAt(line.length - 1) === ']') {
719
+ current = undefined
720
+ const attrlist = line
721
+ .slice(1, -1)
722
+ .trim()
723
+ .replace(/(?:^\w[\w-]*(?!=|[\w-]))?([#.]\w[\w-]*)*/, '')
724
+ .replace(NAMED_ID_ATTR_RX, '')
725
+ if (attrlist) entries.push({ type: 'attrlist', lines: ['[' + attrlist + ']'] })
653
726
  }
654
727
  } else if (belowDoctitle) {
655
728
  break
@@ -657,25 +730,27 @@ function processDocumentHeader (doc, lines, buffer, ignoreLines) {
657
730
  lines[idx] = undefined
658
731
  ignoreLines.push(idx)
659
732
  }
660
- return doc
661
- .getRoles()
662
- .map((role) => '.' + role)
663
- .join('')
733
+ return entries
664
734
  }
665
735
 
666
- function generateSlug (title) {
667
- return title
736
+ function generateSlug (string) {
737
+ if (string === 'index') return string
738
+ return string
668
739
  .toLowerCase()
669
740
  .replace(/<[^>]+>/g, '')
670
- .replace(CharRefRx, (_, name, dec, hex) => {
671
- if (name) return BuiltInNamedEntities[name] ?? '?'
741
+ .replace(CHAR_REF_RX, (_, name, dec, hex) => {
742
+ if (name) return BUILT_IN_NAMED_ENTITIES[name] ?? '?'
672
743
  return String.fromCharCode(dec ? parseInt(dec, 10) : parseInt(hex, 16))
673
744
  })
674
745
  .replace(/[\x27\u2019]/g, '')
675
- .replace(/[^\p{Alpha}0-9\-]/gu, '-')
746
+ .replace(/[^\p{Alpha}0-9-]/gu, '-')
676
747
  .replace(/^-+|-+$|(-)-+/g, '$1')
677
748
  }
678
749
 
750
+ function sanitizeSlug (slug) {
751
+ return slug.replace(/ /g, '-').replace(/[^\p{Alpha}0-9_.-]/gu, '-')
752
+ }
753
+
679
754
  function fixSectionLevels (sections, expectedLevel) {
680
755
  sections.forEach((sect) => {
681
756
  const forceLevel = expectedLevel ?? Math.min(1, sect.getLevel())
@@ -684,62 +759,16 @@ function fixSectionLevels (sections, expectedLevel) {
684
759
  })
685
760
  }
686
761
 
687
- function rewriteStyleAttribute (block, lines, idx, idPrefix, replacementStyle = '') {
688
- let prevLine = lines[idx - 1]
689
- const char0 = prevLine?.charAt()
690
- if (char0) {
691
- if (
692
- (char0 === '.' && /^\.\.?[^ \t.]/.test(prevLine)) ||
693
- (char0 === '[' &&
694
- prevLine.charAt(1) === '[' &&
695
- /^\[\[(?:|[\p{Alpha}_:][\p{Alpha}0-9_\-:.]*(?:, *.+)?)\]\]$/u.test(prevLine))
696
- ) {
697
- return rewriteStyleAttribute(block, lines, idx - 1, idPrefix, replacementStyle)
698
- }
699
- }
700
- let cellSpec
701
- if (
702
- char0 &&
703
- (char0 === '[' || (block.getDocument().isNested() && (cellSpec = prevLine.match(/^([^[|]*)\| *(\[.+)/)))) &&
704
- prevLine.charAt(prevLine.length - 1) === ']'
705
- ) {
706
- if (cellSpec) {
707
- prevLine = cellSpec[2]
708
- cellSpec = cellSpec[1]
709
- }
710
- let rawStyle
711
- const commaIdx = prevLine.indexOf(',')
712
- if (~commaIdx) {
713
- rawStyle = prevLine.slice(1, commaIdx)
714
- if (~rawStyle.indexOf('=')) rawStyle = undefined
715
- } else if (!~prevLine.indexOf('=')) {
716
- rawStyle = prevLine.slice(1, prevLine.length - 1)
717
- }
718
- if (rawStyle) {
719
- if (~rawStyle.indexOf('#')) {
720
- prevLine = prevLine.replace(/#[^.%,\]]+/, `#${idPrefix}${block.getId()}`)
721
- if (replacementStyle) prevLine = prevLine.replace(/^[^#.%,\]]+/, `[${replacementStyle}`)
722
- } else {
723
- prevLine = `[${
724
- replacementStyle ? rawStyle.replace(/^[^.%]*/, replacementStyle) : rawStyle
725
- }#${idPrefix}${block.getId()}${prevLine.slice(rawStyle.length + 1)}`
726
- }
727
- } else {
728
- prevLine = `[${replacementStyle}#${idPrefix}${block.getId()}${rawStyle == null ? ',' : ''}${prevLine.slice(1)}`
729
- }
730
- if (cellSpec) prevLine = `${cellSpec}|${prevLine}`
731
- lines[idx - 1] = prevLine
732
- } else {
733
- lines.splice(idx, 0, `[${replacementStyle}#${idPrefix}${block.getId()}]`)
734
- }
735
- }
736
-
737
- function isResourceSpec (str) {
738
- return !(~str.indexOf(':') && (~str.indexOf('://') || (str.startsWith('data:') && ~str.indexOf(','))))
739
- }
740
-
741
- function getObjectId (obj) {
742
- return global.Opal.uid()
762
+ // NOTE: blank lines at top and bottom of document create mismatch when using line numbers to navigate source lines
763
+ // IMPORTANT: this must not leave behind lines the parser will drop!
764
+ // IDEA: another option is to capture initial lineno of reader and use as offset (but preseves those blank lines)
765
+ function trimAsciiDoc (buffer) {
766
+ return Buffer.from(
767
+ buffer
768
+ .toString()
769
+ .replace(/^(?:[ \t]*\r\n?|[ \t]*\n)+/, '')
770
+ .trimRight()
771
+ )
743
772
  }
744
773
 
745
774
  function safePush (onto, entries) {
@@ -752,4 +781,11 @@ function safePush (onto, entries) {
752
781
  }
753
782
  }
754
783
 
784
+ function generateIdFromTitle (titleAsciiDoc, idSeparators) {
785
+ const Section = this.$class().$const_get('::Asciidoctor::Section')
786
+ const baseId = Section.$generate_id(titleAsciiDoc, this)
787
+ this.getCatalog().refs['$[]='](baseId, true)
788
+ return `_${idSeparators.coordinate}${baseId}`
789
+ }
790
+
755
791
  module.exports = produceAssemblyFile