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

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.
@@ -11,20 +11,22 @@ const {
11
11
  rewriteStyleAttribute,
12
12
  } = require('./util/rewriter')
13
13
  const sanitize = require('./util/sanitize')
14
+ const toHash = require('./util/to-hash')
14
15
  const unconvertInlineAsciiDoc = require('./util/unconvert-inline-asciidoc')
15
16
 
16
17
  const ATTR_ENTRY_RX = /^:(!?[\p{Alpha}0-9_][^:]*):(?: |$)/u
17
18
  const BUILT_IN_NAMED_ENTITIES = { amp: '&', apos: "'", gt: '>', lt: '<', nbsp: ' ', quot: '"' }
18
19
  const CHAR_REF_RX = /&(?:([a-z][a-z]+\d{0,2})|#(?:(\d{2,6})|x([a-z\d]{2,5})));/g
19
20
  const DISCARD_ATTRIBUTE_NAMES = [
20
- 'doctype',
21
- 'leveloffset',
22
- 'preface-title',
23
21
  'assembly-doctype',
24
22
  'assembly-header-attributes',
25
23
  'assembly-navtitle',
26
24
  'assembly-slug',
27
25
  'assembly-style',
26
+ 'doctype',
27
+ 'leveloffset',
28
+ 'page-aliases',
29
+ 'preface-title',
28
30
  'underscore',
29
31
  ]
30
32
  const { NAMED_ID_ATTR_RX } = require('./util/rx')
@@ -42,33 +44,37 @@ function produceAssemblyFile (
42
44
  const pagesByUrl = files.reduce((map, it) => (it.src.family === 'page' ? map.set(it.pub.url, it) : map), new Map())
43
45
  const pagesInOutline = selectPagesInOutline(outline, pagesByUrl)
44
46
  if (outline.urlType === 'internal' && !pagesByUrl.has(outline.pathname) && !outline.items?.length) return
47
+ const { name: component, version } = componentVersion
45
48
  const { rootLevel, xmlIds } = assemblyModel
49
+ const scratchDoc = loadAsciiDoc(
50
+ {
51
+ contents: Buffer.alloc(0),
52
+ src: { component, version, module: 'ROOT', family: 'page', relative: '_.adoc', path: '_.adoc' },
53
+ },
54
+ undefined,
55
+ asciidocConfig
56
+ )
46
57
  const idSeparators = {
47
58
  prefix:
48
59
  'assembler-idprefix' in asciidocConfig.attributes ? (asciidocConfig.attributes['assembler-idprefix'] ?? '') : '_',
49
60
  scope: xmlIds ? '---' : ':::',
50
61
  coordinate: xmlIds ? '----' : ':',
51
- generateIdFromTitle: generateIdFromTitle.bind(
52
- loadAsciiDoc(
53
- { contents: Buffer.alloc(0), src: { family: 'page', relative: 'generate-id-from-title.adoc' } },
54
- undefined,
55
- asciidocConfig
56
- )
57
- ),
62
+ generateIdFromTitle: generateIdFromTitle.bind(scratchDoc),
58
63
  }
59
- const { name: component, version } = componentVersion
60
64
  asciidocConfig = prepareAsciiDocConfig(
61
65
  contentCatalog,
62
66
  { component, version },
63
67
  pagesInOutline,
64
68
  asciidocConfig,
65
69
  assemblyModel,
66
- idSeparators
70
+ idSeparators,
71
+ scratchDoc
67
72
  )
68
- const buffer = mergeAsciiDoc(
73
+ const builder = buildAsciiDocHeader(componentVersion, outline.content, assemblyModel)
74
+ mergeAsciiDoc(
69
75
  loadAsciiDoc,
70
76
  contentCatalog,
71
- buildAsciiDocHeader(componentVersion, outline.content, assemblyModel),
77
+ builder,
72
78
  componentVersion,
73
79
  outline,
74
80
  files,
@@ -78,24 +84,23 @@ function produceAssemblyFile (
78
84
  mutableAttributes,
79
85
  assemblyModel
80
86
  )
81
- if (buffer[buffer.doctypeIdx] === ':doctype: article') buffer.splice(buffer.doctypeIdx, 1)
82
87
  const stem =
83
- (buffer.slug ? sanitizeSlug(buffer.slug) : generateSlug(rootLevel === 0 ? 'index' : buffer.navtitle)) ||
88
+ (builder.slug ? sanitizeSlug(builder.slug) : generateSlug(rootLevel === 0 ? 'index' : builder.navtitle)) ||
84
89
  'export-' + (assemblyModel.stemSeq = (assemblyModel.exportSeq ?? 0) + 1)
85
90
  const downloadStem = [component, version, stem === 'index' ? '' : stem].filter((it) => it).join('-')
86
91
  const file = contentCatalog.createFile({
87
92
  asciidoc: asciidocConfig,
88
93
  assembler: { assembled: pagesInOutline.assembled, downloadStem, rootLevel },
89
- contents: Buffer.from(buffer.join('\n') + '\n'),
94
+ contents: builder.serialize(),
90
95
  src: { component, version, componentVersion, module: 'ROOT', family: 'export', relative: stem + '.adoc' },
91
96
  pub: false,
92
97
  })
93
- file.path = file.src.path = file.out.path // use out path as path so assets can be published to same hierarchy
98
+ file.path = file.out.path // use out path as path so assets can be published to same hierarchy
94
99
  delete file.out
95
100
  return file
96
101
  }
97
102
 
98
- function prepareAsciiDocConfig (contentCatalog, ctx, pagesInOutline, asciidocConfig, assemblyModel, idSeparators) {
103
+ function prepareAsciiDocConfig (contentCatalog, ctx, pagesInOutline, asciidocConfig, assemblyModel, idSeparators, doc) {
99
104
  let attributesModified
100
105
  const configShared = asciidocConfig.$shared
101
106
  const sharedAttributes = asciidocConfig.attributes
@@ -116,7 +121,17 @@ function prepareAsciiDocConfig (contentCatalog, ctx, pagesInOutline, asciidocCon
116
121
  for (const [name, val] of Object.entries(sharedAttributes)) {
117
122
  if (!(val?.constructor === String && ~val.indexOf(':'))) continue
118
123
  if (~val.indexOf('xref:')) {
119
- const newVal = rewriteXrefs(val, contentCatalog, assemblyModel, ctx, false, pagesInOutline, idSeparators)
124
+ const newVal = rewriteXrefs(
125
+ val,
126
+ contentCatalog,
127
+ assemblyModel,
128
+ ctx,
129
+ false,
130
+ pagesInOutline,
131
+ idSeparators,
132
+ null,
133
+ doc
134
+ )
120
135
  if (newVal !== val) (attributesModified ??= {})[name] = newVal
121
136
  }
122
137
  }
@@ -126,25 +141,44 @@ function prepareAsciiDocConfig (contentCatalog, ctx, pagesInOutline, asciidocCon
126
141
  }
127
142
 
128
143
  function buildAsciiDocHeader (componentVersion, navtitle, assemblyModel) {
144
+ const { name: componentName, version, displayVersion, title } = componentVersion
129
145
  const navtitlePlain = sanitize(navtitle)
130
146
  const navtitleAsciiDoc = unconvertInlineAsciiDoc(navtitle)
131
- const doctitle = (componentVersion.title === navtitlePlain ? '' : componentVersion.title + ': ') + navtitleAsciiDoc
132
- const version = componentVersion.version === 'master' ? '' : componentVersion.version
133
- const displayVersion = componentVersion.displayVersion
134
- const buffer = [
135
- `= ${doctitle}`,
136
- ...(version ? [`:revnumber: ${displayVersion}`] : []),
137
- `:doctype: ${assemblyModel.doctype ?? 'book'}`,
138
- ':underscore: _',
139
- // Q: should we pass these via the CLI so they cannot be modified?
140
- `:page-component-name: ${componentVersion.name}`,
141
- `:page-component-version:${version ? ' ' + version : ''}`,
142
- ':page-version: {page-component-version}',
143
- `:page-component-display-version: ${displayVersion}`,
144
- `:page-component-title: ${componentVersion.title}`,
145
- ]
146
- buffer.doctypeIdx = version ? 2 : 1
147
- return Object.assign(buffer, { navtitle })
147
+ const doctitle = (title === navtitlePlain ? '' : title + ': ') + navtitleAsciiDoc
148
+ const versioned = !!(version && version !== 'master')
149
+ return {
150
+ header: [
151
+ `= ${doctitle}`,
152
+ ...(versioned ? [`:revnumber: ${displayVersion}`] : []),
153
+ `:doctype: ${(assemblyModel.doctype ??= 'book')}`,
154
+ ':underscore: _',
155
+ `:page-component-name: ${componentName}`,
156
+ `:page-component-version:${versioned ? ' ' + version : ''}`,
157
+ ':page-version: {page-component-version}',
158
+ `:page-component-display-version: ${displayVersion}`,
159
+ `:page-component-title: ${title}`,
160
+ ],
161
+ currentComponentVersion: componentVersion,
162
+ navtitle,
163
+ setAuthor (author) {
164
+ this.header.splice(1, 0, author)
165
+ },
166
+ setDoctitle (doctitle_) {
167
+ this.header[0] = `= ${doctitle_}`
168
+ },
169
+ setDoctype (doctype) {
170
+ this.header[this.header.findIndex((line) => line.startsWith(':doctype: '))] = `:doctype: ${doctype}`
171
+ },
172
+ setDocid (docid) {
173
+ this.header.unshift(`[#${docid}]`)
174
+ },
175
+ serialize () {
176
+ const normalizedHeader = this.header.filter((it) => it !== ':doctype: article')
177
+ return this.body
178
+ ? Buffer.from(normalizedHeader.join('\n') + '\n\n' + this.body.join('\n') + '\n')
179
+ : Buffer.from(normalizedHeader.join('\n') + '\n')
180
+ },
181
+ }
148
182
  }
149
183
 
150
184
  function selectPagesInOutline (outlineEntry, pagesByUrl, accum) {
@@ -165,7 +199,7 @@ function selectPagesInOutline (outlineEntry, pagesByUrl, accum) {
165
199
  function mergeAsciiDoc (
166
200
  loadAsciiDoc,
167
201
  contentCatalog,
168
- buffer,
202
+ builder,
169
203
  componentVersion,
170
204
  outlineEntry,
171
205
  files,
@@ -174,15 +208,14 @@ function mergeAsciiDoc (
174
208
  asciidocConfig,
175
209
  mutableAttributes,
176
210
  assemblyModel,
177
- lastComponentVersion = componentVersion,
178
211
  level = 0,
179
212
  supportsParts = false
180
213
  ) {
181
214
  const { items = [], roles = [], unresolved, urlType, url, pathname, hash } = outlineEntry
182
215
  // TODO: we could try to be smart about it and make sure the page with fragment is included at least once
183
- if (roles.includes('site-only')) {
184
- buffer.inBody ??= false
185
- return buffer
216
+ if (roles.includes('site-only') || (urlType === 'external' && !URL.canParse(url))) {
217
+ builder.body ??= []
218
+ return builder
186
219
  }
187
220
  const assembled = pagesInOutline.assembled
188
221
  // FIXME: ideally, resource ID would be stored in navigation so we can look up the page more efficiently
@@ -191,18 +224,18 @@ function mergeAsciiDoc (
191
224
  let navtitlePlain = sanitize(navtitle)
192
225
  let navtitleAsciiDoc = unconvertInlineAsciiDoc(navtitle)
193
226
  const { filetype, linkReferenceStyle, pubRoot, sectionMergeStrategy, siteRoot, logger, rootLevel } = assemblyModel
194
- const doctype = assemblyModel.doctype ?? 'book'
195
- const atDocumentRoot = !buffer.inBody
227
+ const doctype = assemblyModel.doctype
228
+ const atDocumentRoot = !builder.body?.length
196
229
  let atBookRoot = atDocumentRoot && !level && doctype === 'book' && (supportsParts = true)
197
230
  const hasItems = items.length > 0
198
231
  if (page && !assembled.pages.has(page)) {
199
232
  const contents = page.src.contents
200
233
  if (contents == null) {
201
- buffer.inBody ??= false
202
- return buffer
234
+ builder.body ??= []
235
+ return builder
203
236
  }
204
237
  const pageAsAsciiDoc = new page.constructor(
205
- Object.assign({}, page, { contents: trimAsciiDoc(contents), mediaType: page.src.mediaType })
238
+ Object.assign({}, page, { contents: Buffer.from(contents.toString().trimEnd()), mediaType: page.src.mediaType })
206
239
  )
207
240
  const doc = loadAsciiDoc(pageAsAsciiDoc, contentCatalog, asciidocConfig)
208
241
  doc.catalog.syntheticIds = {}
@@ -215,25 +248,25 @@ function mergeAsciiDoc (
215
248
  },
216
249
  })
217
250
  : doc.getLogger()
218
- doc.source_header_attributes ??= doc.parent.$to_h()
251
+ doc.source_header_attributes ??= toHash()
219
252
  const isRootPage = atDocumentRoot && !level
220
253
  const { component, version, module: module_, relative, origin } = page.src
221
254
  const doctypeOverride = doc.getAttribute('assembly-doctype')
222
255
  if (doctypeOverride) {
223
- if (doctypeOverride !== doctype) buffer[buffer.doctypeIdx] = `:doctype: ${doctypeOverride}`
256
+ if (doctypeOverride !== doctype) builder.setDoctype(doctypeOverride)
224
257
  if (doctypeOverride !== 'book') atBookRoot = supportsParts = false
225
258
  }
226
- if (isRootPage && doc.hasAttribute('assembly-slug')) buffer.slug = doc.getAttribute('assembly-slug')
259
+ if (isRootPage && doc.hasAttribute('assembly-slug')) builder.slug = doc.getAttribute('assembly-slug')
227
260
  let hasAssemblyNavtitleAttr
228
261
  if ((hasAssemblyNavtitleAttr = !!doc.getAttribute('assembly-navtitle'))) {
229
262
  navtitleAsciiDoc = doc.getAttribute('assembly-navtitle')
230
263
  navtitlePlain = sanitize((navtitle = doc.$apply_reftext_subs(navtitleAsciiDoc)))
231
- if (buffer.inBody == null) {
232
- buffer.navtitle = navtitle
264
+ if (builder.body == null) {
265
+ builder.navtitle = navtitle
233
266
  if (rootLevel) {
234
- const doctitle =
267
+ builder.setDoctitle(
235
268
  (componentVersion.title === navtitlePlain ? '' : componentVersion.title + ': ') + navtitleAsciiDoc
236
- buffer[0] = `= ${doctitle}`
269
+ )
237
270
  }
238
271
  }
239
272
  }
@@ -241,7 +274,7 @@ function mergeAsciiDoc (
241
274
  const docname = doc.getAttribute('docname')
242
275
  const pageId = generateScopedId(page.src, componentVersion, idSeparators, filetype)
243
276
  const pageIdLeader = pageId + idSeparators.scope
244
- let pageFragment = ''
277
+ let pageFragment = `#${pageId}`
245
278
  let pageRoles = []
246
279
  let pageStyle = doc.getAttribute('assembly-style', '')
247
280
  if (!pageStyle && atBookRoot && rootLevel === 0 && !doc.source_header_attributes['$key?']('assembly-style')) {
@@ -259,13 +292,10 @@ function mergeAsciiDoc (
259
292
  let nextSectionLevel = 1
260
293
  const lines = doc.getSourceLines()
261
294
  const ignoreLines = []
262
- if (!buffer.inBody) {
263
- buffer.inBody = true
264
- buffer.endHeaderIdx = buffer.length - 1
265
- }
266
- buffer.push('')
295
+ const buffer = (builder.body ??= [])
296
+ if (buffer.length) buffer.push('')
267
297
  buffer.push(`:docname: ${docname}`)
268
- if (component !== lastComponentVersion.name) {
298
+ if (component !== builder.currentComponentVersion.name) {
269
299
  const thisComponentVersion =
270
300
  component === componentVersion.name && version === componentVersion.version
271
301
  ? componentVersion
@@ -276,7 +306,7 @@ function mergeAsciiDoc (
276
306
  buffer.push(':page-version: {page-component-version}')
277
307
  buffer.push(`:page-component-display-version: ${thisComponentVersion.displayVersion}`)
278
308
  buffer.push(`:page-component-title: ${thisComponentVersion.title}`)
279
- lastComponentVersion = thisComponentVersion
309
+ builder.currentComponentVersion = thisComponentVersion
280
310
  }
281
311
  }
282
312
  buffer.push(`:page-module: ${module_}`)
@@ -286,13 +316,10 @@ function mergeAsciiDoc (
286
316
  buffer.push(`:page-origin-refname: ${origin.branch || origin.tag}`)
287
317
  buffer.push(`:page-origin-reftype: ${origin.branch ? 'branch' : 'tag'}`)
288
318
  buffer.push(`:page-origin-refhash: ${origin.worktree ? '(worktree)' : origin.refhash}`)
289
- let headerHasBlockAttrs
290
319
  if (doc.hasHeader()) {
291
320
  for (const entry of processDocumentHeader(doc, lines, ignoreLines, isRootPage)) {
292
321
  if (entry.type === 'author_line') {
293
- buffer.splice(1, 0, entry.lines[0])
294
- buffer.doctypeIdx += 1
295
- buffer.endHeaderIdx += 1
322
+ builder.setAuthor(entry.lines[0])
296
323
  } else if (entry.type === 'attribute_entry') {
297
324
  const name = entry.name
298
325
  if (!entry.negated && !doc.isAttributeLocked(name)) {
@@ -300,7 +327,7 @@ function mergeAsciiDoc (
300
327
  if (
301
328
  name.endsWith('-image') &&
302
329
  (val = doc.getAttribute(name)) &&
303
- (newVal = rewriteImageAttr(val, contentCatalog, assemblyModel, page.src, assembled.assets))
330
+ (newVal = rewriteImageAttr(val, contentCatalog, assemblyModel, page.src, assembled.assets)) != null
304
331
  ) {
305
332
  if (newVal !== val) entry.lines = [`:${name}: ${newVal}`]
306
333
  } else if (~(val = entry.lines.join('\n').substring(name.length + 3)).indexOf(':')) {
@@ -333,14 +360,9 @@ function mergeAsciiDoc (
333
360
  if (newVal !== val) entry.lines = `:${entry.name}: ${newVal}`.split('\n')
334
361
  }
335
362
  }
336
- if (entry.promote) {
337
- buffer.splice(buffer.endHeaderIdx + 1, 0, ...entry.lines)
338
- buffer.endHeaderIdx += entry.lines.length
339
- } else {
340
- buffer.push(...entry.lines)
341
- }
363
+ ;(entry.promote ? builder.header : buffer).push(...entry.lines)
342
364
  } else {
343
- if (entry.type === 'attrlist') headerHasBlockAttrs = true
365
+ if (entry.type === 'attrlist') builder.headerHasBlockAttrs = true
344
366
  buffer.push(...entry.lines)
345
367
  }
346
368
  }
@@ -364,15 +386,15 @@ function mergeAsciiDoc (
364
386
  if (htitleOverride != null) {
365
387
  const keepSections = hasSections && sectionMergeStrategy !== 'discrete'
366
388
  if (htitleOverride === '') {
367
- if (headerHasBlockAttrs || keepSections) {
389
+ if (builder.headerHasBlockAttrs || keepSections) {
368
390
  htitleAsciiDoc = '{empty}'
369
391
  notitle = true
370
392
  } else {
371
- if (hasPrefaceTitleAttr) buffer.splice(++buffer.endHeaderIdx, 0, ':preface-title:')
393
+ if (hasPrefaceTitleAttr) builder.header.push(':preface-title:')
372
394
  htitleAsciiDoc = undefined
373
395
  }
374
- } else if (hasPrefaceTitleAttr && !headerHasBlockAttrs && !keepSections) {
375
- buffer.splice(++buffer.endHeaderIdx, 0, `:preface-title: ${unconvertInlineAsciiDoc(htitleOverride)}`)
396
+ } else if (hasPrefaceTitleAttr && !builder.headerHasBlockAttrs && !keepSections) {
397
+ builder.header.push(`:preface-title: ${unconvertInlineAsciiDoc(htitleOverride)}`)
376
398
  htitleAsciiDoc = undefined
377
399
  } else {
378
400
  htitleAsciiDoc = unconvertInlineAsciiDoc(htitleOverride)
@@ -383,10 +405,7 @@ function mergeAsciiDoc (
383
405
  if (atDocumentRoot) {
384
406
  // Q: should we use htitleAsciiDoc to generate ID if not {empty} instead of pageStyle
385
407
  pageFragment = `#_${idSeparators.coordinate}${pageIdLeader}${pageStyle}`
386
- buffer.unshift(`[#${pageId}]`)
387
- buffer.doctypeIdx += 1
388
- } else {
389
- pageFragment = `#${pageId}`
408
+ builder.setDocid(pageId)
390
409
  }
391
410
  heading = { title: htitleAsciiDoc, level: part ? 1 : 2 }
392
411
  if (notitle) heading.notitle = true
@@ -396,14 +415,12 @@ function mergeAsciiDoc (
396
415
  if (atDocumentRoot && rootLevel === 0 && navtitlePlain === componentVersion.title && !hasAssemblyNavtitleAttr) {
397
416
  level--
398
417
  } else {
399
- pageFragment = `#${pageId}`
400
418
  if (part && level === 1) level--
401
419
  if ((heading = { title: navtitleAsciiDoc, level: level + 1 }).level > 6) {
402
420
  Object.assign(heading, { level: 6, style: `discrete.h${heading.level}` })
403
421
  }
404
422
  }
405
423
  } else if (atDocumentRoot && rootLevel === 0 && hasAssemblyNavtitleAttr) {
406
- pageFragment = `#${pageId}`
407
424
  heading = { title: navtitleAsciiDoc, level: (nextSectionLevel = part ? 1 : 2) }
408
425
  } else if (atBookRoot) {
409
426
  nextSectionLevel = undefined
@@ -414,16 +431,17 @@ function mergeAsciiDoc (
414
431
  buffer.push(`[${heading.style ?? pageStyle}${pageFragment}${rolesAttr}${notitleAttrs}]`)
415
432
  buffer.push(`${'='.repeat(heading.level)} ${heading.title}`)
416
433
  } else if (atDocumentRoot) {
417
- buffer.unshift(`[#${pageId}]`)
418
- buffer.doctypeIdx += 1
434
+ builder.setDocid(pageId)
419
435
  }
436
+ if (isRootPage || (atDocumentRoot && !heading)) assembled.pages.rootPageFragment = pageFragment
437
+ assembled.pages.set(page, pageFragment)
420
438
  if (sectionMergeStrategy === 'enclose' && hasItems && hasSections && !(atDocumentRoot && heading)) {
421
439
  // TODO: make overview section title configurable
422
440
  //let overviewTitle = doc.getDocumentTitle()
423
441
  //if (overviewTitle === navtitle) overviewTitle = doc.getAttribute('overview-title', 'Overview')
424
442
  const overviewTitle = doc.getAttribute('overview-title', 'Overview')
425
443
  const syntheticId = idSeparators.generateIdFromTitle(overviewTitle, idSeparators, pageIdLeader)
426
- if (heading) buffer.push('')
444
+ if (heading && buffer.length) buffer.push('')
427
445
  let hlevel = level + (nextSectionLevel = 2)
428
446
  if (hlevel > 6) {
429
447
  const blockStyle = `discrete.h${hlevel}`
@@ -434,7 +452,6 @@ function mergeAsciiDoc (
434
452
  }
435
453
  buffer.push(`${'='.repeat(hlevel)} ${overviewTitle}`)
436
454
  }
437
- assembled.pages.set(page, pageFragment)
438
455
  if (hasSections) fixSectionLevels(doc.getSections(), nextSectionLevel)
439
456
  const allBlocks = doc.findBy({ traverse_documents: true }, (it) =>
440
457
  it.getContext() === 'document'
@@ -444,7 +461,7 @@ function mergeAsciiDoc (
444
461
  if (doc.getDoctype() === 'manpage') {
445
462
  const firstSectionIdx = hasSections ? doc.getSections()[0].getLineNumber() - 1 : lines.length
446
463
  for (let idx = 0; idx < firstSectionIdx; idx++) {
447
- if (~ignoreLines.indexOf(idx)) continue
464
+ if (ignoreLines.includes(idx)) continue
448
465
  const line = lines[idx]
449
466
  if (line.startsWith('== ') && line.length > 3) {
450
467
  allBlocks.unshift({
@@ -488,7 +505,7 @@ function mergeAsciiDoc (
488
505
  })
489
506
  let skipping
490
507
  for (let idx = 0, lastIdx = lines.length - 1; idx <= lastIdx; idx++) {
491
- if (~ignoreLines.indexOf(idx)) continue
508
+ if (ignoreLines.includes(idx)) continue
492
509
  let line = lines[idx]
493
510
  if (line.startsWith('//')) {
494
511
  if (line.charAt(2) !== '/') continue
@@ -604,10 +621,13 @@ function mergeAsciiDoc (
604
621
  buffer,
605
622
  lines.filter((it) => it !== undefined)
606
623
  )
607
- const attributeEntries = Object.entries(doc.source_header_attributes.$$smap)
624
+ const attributeEntries = doc.source_header_attributes.$entries()
608
625
  if (attributeEntries.length) {
609
626
  const resolvedAttributeEntries = attributeEntries.reduce(
610
627
  (accum, [name, val]) => {
628
+ if (val) {
629
+ if (val['$nil?']()) val = null
630
+ }
611
631
  // Q: couldn't we just check if attribute is locked?
612
632
  if (name in mutableAttributes) {
613
633
  const initialVal = mutableAttributes[name]
@@ -626,12 +646,11 @@ function mergeAsciiDoc (
626
646
  if (resolvedAttributeEntries.length > 1) safePush(buffer, resolvedAttributeEntries)
627
647
  }
628
648
  } else if (level && !hash) {
649
+ const buffer = (builder.body ??= [])
629
650
  if (atDocumentRoot && rootLevel === 0 && navtitlePlain === componentVersion.title) {
630
- buffer.inBody ??= false
631
651
  level--
632
652
  } else {
633
- buffer.inBody = true
634
- buffer.push('')
653
+ if (buffer.length) buffer.push('')
635
654
  const syntheticId = idSeparators.generateIdFromTitle(navtitleAsciiDoc, idSeparators)
636
655
  let sectionTitle = navtitleAsciiDoc
637
656
  if (urlType === 'external') {
@@ -647,38 +666,39 @@ function mergeAsciiDoc (
647
666
  }
648
667
  }
649
668
  }
669
+ if (supportsParts && level === 1 && roles.includes('part')) {
670
+ roles.splice(roles.indexOf('part', 1))
671
+ level--
672
+ }
650
673
  const hlevel = level > 5 ? 6 : level + 1
651
674
  const hroles = urlType === 'internal' ? roles.slice(1) : roles
652
675
  buffer.push(`[${hlevel > 6 ? 'discrete' : ''}#${syntheticId}${hroles.reduce((str, it) => str + '.' + it, '')}]`)
653
676
  buffer.push(`${'='.repeat(hlevel)} ${sectionTitle}`)
654
677
  }
655
678
  }
656
- if (hasItems) {
657
- const nextLevel = level + 1
658
- // NOTE: drop first child if same as parent; should we keep if content is different?
659
- ;(urlType === 'internal' && items[0].urlType === 'internal' && pathname === items[0].url && !items[0].items?.length
660
- ? items.slice(1)
661
- : items
662
- ).forEach((item) => {
663
- mergeAsciiDoc(
664
- loadAsciiDoc,
665
- contentCatalog,
666
- buffer,
667
- componentVersion,
668
- item,
669
- files,
670
- pagesInOutline,
671
- idSeparators,
672
- asciidocConfig,
673
- mutableAttributes,
674
- assemblyModel,
675
- lastComponentVersion,
676
- nextLevel,
677
- atBookRoot
678
- )
679
- })
680
- }
681
- return buffer
679
+ if (!hasItems) return
680
+ const nextLevel = level + 1
681
+ // NOTE: drop first child if same as parent; should we keep if content is different?
682
+ ;(urlType === 'internal' && items[0].urlType === 'internal' && pathname === items[0].url && !items[0].items?.length
683
+ ? items.slice(1)
684
+ : items
685
+ ).forEach((item) => {
686
+ mergeAsciiDoc(
687
+ loadAsciiDoc,
688
+ contentCatalog,
689
+ builder,
690
+ componentVersion,
691
+ item,
692
+ files,
693
+ pagesInOutline,
694
+ idSeparators,
695
+ asciidocConfig,
696
+ mutableAttributes,
697
+ assemblyModel,
698
+ nextLevel,
699
+ atBookRoot
700
+ )
701
+ })
682
702
  }
683
703
 
684
704
  function processDocumentHeader (doc, lines, ignoreLines, isRootPage) {
@@ -717,7 +737,7 @@ function processDocumentHeader (doc, lines, ignoreLines, isRootPage) {
717
737
  }
718
738
  const line = lines[idx]
719
739
  if (open === ':' || open === '-:') {
720
- if (open === ':') current.lines.push(line)
740
+ if (open === ':') current.appendLine(line)
721
741
  if (!line?.endsWith(' \\')) current = open = undefined
722
742
  } else if (line) {
723
743
  let attributeEntryMatch
@@ -725,13 +745,14 @@ function processDocumentHeader (doc, lines, ignoreLines, isRootPage) {
725
745
  if (chr0 === '/' && line.charAt(1) === '/') {
726
746
  if (line.startsWith('////') && line === '/'.repeat(line.length)) {
727
747
  if (open) {
728
- current.lines.push(line)
748
+ current.appendLine(line)
729
749
  if (open === line) current = open = undefined
730
750
  } else {
731
751
  entries.push((current = { type: 'block_comment', lines: [(open = line)] }))
752
+ current.appendLine = Array.prototype.push.bind(current.lines)
732
753
  }
733
754
  } else if (open) {
734
- current.lines.push(line)
755
+ current.appendLine(line)
735
756
  } else if (belowDoctitle && line.charAt(2) === '/') {
736
757
  break
737
758
  } else {
@@ -739,7 +760,7 @@ function processDocumentHeader (doc, lines, ignoreLines, isRootPage) {
739
760
  current = undefined
740
761
  }
741
762
  } else if (open) {
742
- current.lines.push(line)
763
+ current.appendLine(line)
743
764
  } else if (chr0 === ':' && ~line.indexOf(':', 2) && (attributeEntryMatch = line.match(ATTR_ENTRY_RX))) {
744
765
  let name = attributeEntryMatch[1]
745
766
  const negated = name.charAt() === '!' || name.charAt(name.length - 1) === '!'
@@ -748,7 +769,9 @@ function processDocumentHeader (doc, lines, ignoreLines, isRootPage) {
748
769
  if (line.endsWith(' \\')) open = '-:' // disallow value continuation
749
770
  } else {
750
771
  if (line.endsWith(' \\')) open = ':'
751
- entries.push((current = { type: 'attribute_entry', name, negated, lines: [line], lineno: idx + 1 }))
772
+ entries.push((current = { type: 'attribute_entry', name, negated, lines: [], lineno: idx + 1 }))
773
+ current.lines.push(negated ? `:!${name}:` : line)
774
+ current.appendLine = Array.prototype.push.bind(negated ? [] : current.lines)
752
775
  if (assemblyHeaderAttributes.has(name)) current.promote = true
753
776
  }
754
777
  } else if (belowDoctitle) {
@@ -799,36 +822,22 @@ function fixSectionLevels (sections, expectedLevel) {
799
822
  })
800
823
  }
801
824
 
802
- // NOTE: blank lines at top and bottom of document create mismatch when using line numbers to navigate source lines
803
- // IMPORTANT: this must not leave behind lines the parser will drop!
804
- // IDEA: another option is to capture initial lineno of reader and use as offset (but preseves those blank lines)
805
- function trimAsciiDoc (buffer) {
806
- return Buffer.from(
807
- buffer
808
- .toString()
809
- .replace(/^(?:[ \t]*\r\n?|[ \t]*\n)+/, '')
810
- .trimRight()
811
- )
812
- }
813
-
814
825
  function safePush (onto, entries) {
815
826
  try {
816
827
  onto.push(...entries)
817
- } catch (err) {
818
- /* istanbul ignore if */
819
- if (!(err instanceof RangeError)) throw err
828
+ } catch {
820
829
  for (const e of entries) onto.push(e)
821
830
  }
822
831
  }
823
832
 
824
833
  function generateIdFromTitle (titleAsciiDoc, idSeparators, idLeader) {
825
- const Section = this.$class().$const_get('::Asciidoctor::Section')
834
+ const Section = this.$class().$$base_module.Section
826
835
  const baseId = Section.$generate_id(titleAsciiDoc, this) // refs in catalog will always be empty
827
836
  let id = `_${idSeparators.coordinate}${idLeader ?? ''}${baseId}`
828
837
  const syntheticIds = (this.catalog.syntheticIds ??= {})
829
838
  if (id in syntheticIds) {
830
839
  const sep = (this.getAttribute('idseparator') ?? '_').charAt()
831
- let cnt = this.$class().$const_get('::Asciidoctor::Compliance').unique_id_start_index
840
+ let cnt = this.$class().$$base_module.Compliance.unique_id_start_index
832
841
  let candidate
833
842
  while ((candidate = `${id}${sep}${cnt}`) in syntheticIds) cnt++
834
843
  id = candidate