@antora/assembler 1.0.0-alpha.9 → 1.0.0-beta.2

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,64 +1,63 @@
1
1
  'use strict'
2
2
 
3
- const File = require('vinyl')
3
+ const createAsciiDocFile = require('./util/create-asciidoc-file')
4
4
  const path = require('node:path/posix')
5
5
  const sanitize = require('./util/sanitize')
6
6
  const unconvertInlineAsciiDoc = require('./util/unconvert-inline-asciidoc')
7
7
 
8
- function produceAggregateDocument (
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(' ')
13
+
14
+ function produceAssemblyFile (
9
15
  loadAsciiDoc,
10
16
  contentCatalog,
11
17
  componentVersion,
12
18
  outline,
13
- doctype,
14
- pages,
19
+ files,
15
20
  asciidocConfig,
16
21
  mutableAttributes,
17
- sectionMergeStrategy = 'discrete'
22
+ assemblyModel
18
23
  ) {
19
- const pagesInOutline = selectPagesInOutline(outline, pages)
24
+ const pagesByUrl = files.reduce((map, it) => (it.src.family === 'page' ? map.set(it.pub.url, it) : map), new Map())
25
+ if (outline.urlType === 'internal' && !pagesByUrl.get(outline.url) && !(outline.items || []).length) return
26
+ const pagesInOutline = selectPagesInOutline(outline, pagesByUrl, componentVersion)
20
27
  const navtitle = outline.content
21
- const templateFile = contentCatalog.addFile({
22
- src: {
23
- component: componentVersion.name,
24
- version: componentVersion.version,
25
- module: 'ROOT',
26
- family: 'page',
27
- relative: generateSlug(navtitle),
28
- },
29
- })
30
- const { dir: outDir, name: outName } = path.parse(templateFile.out.path)
31
- const path_ = outName === templateFile.src.relative ? path.join(outDir, outName + '.adoc') : outDir + '.adoc'
32
- contentCatalog.removeFile(templateFile)
33
- const header = buildAsciiDocHeader(componentVersion, navtitle, doctype)
34
- const body = aggregateAsciiDoc(
28
+ const buffer = mergeAsciiDoc(
35
29
  loadAsciiDoc,
36
30
  contentCatalog,
37
- header,
31
+ buildAsciiDocHeader(componentVersion, navtitle, assemblyModel),
38
32
  componentVersion,
39
33
  outline,
34
+ files,
40
35
  pagesInOutline,
41
36
  asciidocConfig,
42
37
  mutableAttributes,
43
- sectionMergeStrategy
38
+ assemblyModel
44
39
  )
45
- return new File({
46
- aggregate: true,
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('-')
45
+ return createAsciiDocFile(contentCatalog, {
47
46
  asciidoc: asciidocConfig,
48
- contents: Buffer.from([...header, ...body].join('\n') + '\n'),
49
- mediaType: 'text/asciidoc',
50
- path: path_,
47
+ assembler: { assembled: pagesInOutline.assembled, downloadStem, rootLevel },
48
+ contents: Buffer.from(buffer.join('\n') + '\n'),
51
49
  src: {
52
50
  component: componentVersion.name,
53
51
  version: componentVersion.version,
54
- basename: path.basename(path_),
55
- stem: path.basename(path_, '.adoc'),
56
- extname: '.adoc',
52
+ module: 'ROOT',
53
+ family: 'export',
54
+ relative: stem + '.adoc',
57
55
  },
58
56
  })
59
57
  }
60
58
 
61
- function buildAsciiDocHeader (componentVersion, navtitle, doctype = 'book') {
59
+ function buildAsciiDocHeader (componentVersion, navtitle, assemblyModel) {
60
+ const doctype = assemblyModel.doctype ?? 'book'
62
61
  const navtitlePlain = sanitize(navtitle)
63
62
  const navtitleAsciiDoc = unconvertInlineAsciiDoc(navtitle)
64
63
  let doctitle = navtitleAsciiDoc
@@ -67,7 +66,7 @@ function buildAsciiDocHeader (componentVersion, navtitle, doctype = 'book') {
67
66
  return [
68
67
  `= ${doctitle}`,
69
68
  ...(version ? [`:revnumber: ${version}`] : []),
70
- ...(doctype === 'article' ? [] : [`:doctype: ${doctype}`]),
69
+ ...(doctype === 'article' ? [] : [`:doctype: ${doctype ?? 'book'}`]),
71
70
  ':underscore: _',
72
71
  // Q: should we pass these via the CLI so they cannot be modified?
73
72
  `:page-component-name: ${componentVersion.name}`,
@@ -78,46 +77,52 @@ function buildAsciiDocHeader (componentVersion, navtitle, doctype = 'book') {
78
77
  ]
79
78
  }
80
79
 
81
- function selectPagesInOutline (outlineEntry, pages) {
82
- const page = outlineEntry.urlType === 'internal' ? pages.find((it) => it.pub.url === outlineEntry.url) : undefined
83
- return (outlineEntry.items || []).reduce(
84
- (accum, item) => new Map([...accum, ...selectPagesInOutline(item, pages)]),
85
- new Map(
86
- page && [
87
- [`${page.src.module === 'ROOT' ? '' : page.src.module + ':'}${page.src.relative}`, page],
88
- [page.pub.url, page],
89
- ]
90
- )
91
- )
80
+ function selectPagesInOutline (outlineEntry, pagesByUrl, componentVersion, accum) {
81
+ accum ??= Object.assign(new Map(), { assembled: { pages: new Map(), assets: new Set() } })
82
+ const page = outlineEntry.urlType === 'internal' ? pagesByUrl.get(outlineEntry.url) : undefined
83
+ 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)
88
+ }
89
+ for (const item of outlineEntry.items || []) selectPagesInOutline(item, pagesByUrl, componentVersion, accum)
90
+ return accum
92
91
  }
93
92
 
94
- function aggregateAsciiDoc (
93
+ function mergeAsciiDoc (
95
94
  loadAsciiDoc,
96
95
  contentCatalog,
97
- header,
96
+ buffer,
98
97
  componentVersion,
99
98
  outlineEntry,
99
+ files,
100
100
  pagesInOutline,
101
101
  asciidocConfig,
102
102
  mutableAttributes,
103
- sectionMergeStrategy,
103
+ assemblyModel,
104
104
  lastComponentVersion = componentVersion,
105
- level = 0
105
+ level = 0,
106
+ supportsParts = false
106
107
  ) {
107
- const buffer = []
108
108
  // TODO: we could try to be smart about it and make sure the page with fragment is included at least once
109
109
  if (outlineEntry.hash) return buffer
110
110
  const { content: navtitle, items = [], unresolved, urlType, url } = outlineEntry
111
+ const atDocumentRoot = !buffer.inBody
112
+ const atBookRoot = atDocumentRoot && !level && assemblyModel.doctype === 'book' && (supportsParts = true)
111
113
  const hasItems = items.length > 0
112
114
  const navtitlePlain = sanitize(navtitle)
113
115
  const navtitleAsciiDoc = unconvertInlineAsciiDoc(navtitle)
114
116
  const siteUrl = ((val) => {
115
117
  if (!val || val === '/') return ''
116
118
  return val.charAt(val.length - 1) === '/' ? val.slice(0, val.length - 1) : val
117
- })(asciidocConfig.attributes['site-url'])
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
118
123
  // FIXME: ideally, resource ID would be stored in navigation so we can look up the page more efficiently
119
124
  let page = urlType === 'internal' && !unresolved ? pagesInOutline.get(url) : undefined
120
- if (page && pagesInOutline.aggregated?.includes(page)) page = undefined
125
+ if (page && pagesInOutline.assembled.pages.has(page)) page = undefined
121
126
  if (page) {
122
127
  let contents = page.src.contents
123
128
  if (contents == null) return buffer
@@ -130,20 +135,51 @@ function aggregateAsciiDoc (
130
135
  .replace(/^(?:[ \t]*\r\n?|[ \t]*\n)+/, '')
131
136
  .trimRight()
132
137
  )
133
- ;(pagesInOutline.aggregated ??= []).push(page)
134
- page = new page.constructor(Object.assign({}, page, { contents, mediaType: 'text/asciidoc' }))
135
138
  const { component, version, module: module_, relative, origin } = page.src
136
- const doc = loadAsciiDoc(page, contentCatalog, asciidocConfig)
137
- const refs = doc.getCatalog().refs
139
+ const topicPrefix = ~relative.indexOf('/') ? path.dirname(relative) + '/' : ''
140
+ const pageAsAsciiDoc = new page.constructor(Object.assign({}, page, { contents, mediaType: page.src.mediaType }))
141
+ 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)
152
+ }
153
+ }
138
154
  // NOTE: in Antora, docname is relative src path from module without file extension
139
155
  const docname = doc.getAttribute('docname')
140
- const docnameForId = docname.replace(/[/]/g, '::').replace(/[.]/g, '-')
141
- const scopeId = component !== componentVersion.name
142
- const idprefix =
143
- (scopeId ? component + ':' : '') +
144
- (module_ === 'ROOT' ? (scopeId ? ':' : '') : module_ + ':') +
145
- docnameForId +
146
- ':::'
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
168
+ let pageFragment = ''
169
+ let pageRoles = ''
170
+ let pageStyle = doc.getAttribute('assembly-style', '')
171
+ let part
172
+ if ((part = pageStyle === 'part')) {
173
+ pageStyle = ''
174
+ if (!supportsParts) part = undefined
175
+ } else if ((part = pageStyle.endsWith('-part'))) {
176
+ pageStyle = pageStyle.slice(0, -5)
177
+ if (!supportsParts) part = undefined
178
+ }
179
+ let nextSectionLevel = 1
180
+ const lines = doc.getSourceLines()
181
+ const ignoreLines = []
182
+ buffer.inBody = true
147
183
  buffer.push('')
148
184
  buffer.push(`:docname: ${docname}`)
149
185
  if (component !== lastComponentVersion.name) {
@@ -168,25 +204,50 @@ function aggregateAsciiDoc (
168
204
  buffer.push(`:page-origin-refname: ${origin.branch || origin.tag}`)
169
205
  buffer.push(`:page-origin-reftype: ${origin.branch ? 'branch' : 'tag'}`)
170
206
  buffer.push(`:page-origin-refhash: ${origin.worktree ? '(worktree)' : origin.refhash}`)
171
- let enclosed
172
- // NOTE: if level is 0, doctitle has already been added and we're in the document header
173
- if (level) {
174
- if (level === 1 && navtitlePlain === componentVersion.title) {
175
- level--
207
+ if (doc.hasHeader()) pageRoles = processDocumentHeader(doc, lines, buffer, ignoreLines)
208
+ let heading
209
+ if (pageStyle && (part && level === 1 ? (level = 0) : level) === 0) {
210
+ let htitleAsciiDoc = navtitleAsciiDoc
211
+ let htitlePlain = navtitlePlain
212
+ let htitleOverride
213
+ if (
214
+ (htitleOverride = pageStyle === 'preface' ? doc.getAttribute('preface-title') : undefined) ||
215
+ (atDocumentRoot && (htitleOverride = outlineEntry.navtitle))
216
+ ) {
217
+ htitleAsciiDoc = unconvertInlineAsciiDoc(htitleOverride)
218
+ htitlePlain = sanitize(htitleOverride)
219
+ }
220
+ if (atDocumentRoot && pageStyle === 'preface' && htitlePlain === componentVersion.title) {
221
+ assemblyModel = Object.assign({}, assemblyModel, { sectionMergeStrategy: 'discrete' })
176
222
  } else {
177
- let hlevel = level + 1
178
- if (hlevel > 6) {
179
- hlevel = 6
180
- buffer.push(`[discrete#${idprefix}]`)
223
+ if (atDocumentRoot) {
224
+ pageFragment = `#${idPrefix}${doc.getId() ?? pageStyle}`
225
+ buffer.unshift(`[#${idScope}]`)
181
226
  } else {
182
- buffer.push(`[#${idprefix}]`)
227
+ pageFragment = `#${idScope}`
228
+ }
229
+ heading = { title: htitleAsciiDoc, level: part ? 1 : 2 }
230
+ nextSectionLevel++
231
+ }
232
+ } else if (level) {
233
+ if (atDocumentRoot && navtitlePlain === componentVersion.title) {
234
+ level--
235
+ } else {
236
+ pageFragment = `#${idScope}`
237
+ if (part && level === 1) level--
238
+ if ((heading = { title: navtitleAsciiDoc, level: level + 1 }).level > 6) {
239
+ Object.assign(heading, { level: 6, style: `discrete.h${heading.level}` })
183
240
  }
184
- buffer.push(`${'='.repeat(hlevel)} ${navtitleAsciiDoc}`)
185
241
  }
186
- } else {
187
- header.unshift(`[#${idprefix}]`)
188
242
  }
189
- if (sectionMergeStrategy === 'enclose' && hasItems && doc.hasSections()) {
243
+ if (heading) {
244
+ buffer.push(`[${heading.style ?? pageStyle}${pageFragment}${pageRoles}]`)
245
+ buffer.push(`${'='.repeat(heading.level)} ${heading.title}`)
246
+ } else if (atDocumentRoot) {
247
+ buffer.unshift(`[#${idScope}]`)
248
+ }
249
+ let enclosed
250
+ if (assemblyModel.sectionMergeStrategy === 'enclose' && hasItems && doc.hasSections()) {
190
251
  enclosed = true
191
252
  // TODO: make overview section title configurable
192
253
  //let overviewTitle = doc.getDocumentTitle()
@@ -205,23 +266,43 @@ function aggregateAsciiDoc (
205
266
  }
206
267
  let hlevel = level + 2
207
268
  if (hlevel > 6) {
269
+ const blockStyle = `discrete.h${hlevel}`
208
270
  hlevel = 6
209
- buffer.push(syntheticId ? `[discrete#${syntheticId}]` : '[discrete]')
271
+ buffer.push(syntheticId ? `[${blockStyle}#${syntheticId}]` : `[${blockStyle}]`)
210
272
  } else if (syntheticId) {
211
273
  buffer.push(`[#${syntheticId}]`)
212
274
  }
213
275
  buffer.push(`${'='.repeat(hlevel)} ${overviewTitle}`)
214
276
  if (toggleSectids) buffer.push(':sectids:')
215
277
  }
216
- const lines = doc.getSourceLines()
217
- const ignoreLines = []
218
- // TODO: think more about when multipart is allowed; perhaps configurable
219
- if (doc.hasSections()) fixSectionLevels(doc.getSections(), level === 0)
278
+ pagesInOutline.assembled.pages.set(page, pageFragment)
279
+ if (doc.hasSections()) {
280
+ fixSectionLevels(doc.getSections(), atBookRoot && !pageStyle ? undefined : nextSectionLevel)
281
+ }
220
282
  const allBlocks = doc.findBy({ traverse_documents: true }, (it) =>
221
283
  it.getContext() === 'document'
222
284
  ? it.getDocument().isNested()
223
285
  : !(it.getContext() === 'table_cell' && it.getStyle() === 'asciidoc')
224
286
  )
287
+ if (doc.getDoctype() === 'manpage') {
288
+ const firstSectionIdx = doc.hasSections() ? doc.getSections()[0].getLineNumber() - 1 : lines.length
289
+ for (let idx = 0; idx < firstSectionIdx; idx++) {
290
+ if (~ignoreLines.indexOf(idx)) continue
291
+ const line = lines[idx]
292
+ if (line.startsWith('== ') && line.length > 3) {
293
+ allBlocks.unshift({
294
+ getContext: () => 'section',
295
+ getDocument: () => doc,
296
+ getId: () => doc.getAttribute('manname-id'),
297
+ getLineNumber: () => idx + 1,
298
+ getSectionName: () => undefined,
299
+ level: 1,
300
+ })
301
+ break
302
+ }
303
+ }
304
+ }
305
+ const refs = doc.getCatalog().refs
225
306
  allBlocks.forEach((block) => {
226
307
  const contentModel = block.content_model
227
308
  if (
@@ -249,7 +330,7 @@ function aggregateAsciiDoc (
249
330
  }
250
331
  })
251
332
  let skipping
252
- for (let idx = 0, len = lines.length; idx < len; idx++) {
333
+ for (let idx = 0, lastIdx = lines.length - 1; idx <= lastIdx; idx++) {
253
334
  if (~ignoreLines.indexOf(idx)) continue
254
335
  let line = lines[idx]
255
336
  if (line.startsWith('//')) {
@@ -265,7 +346,11 @@ function aggregateAsciiDoc (
265
346
  } else if (skipping) {
266
347
  continue
267
348
  }
268
- if (line.charAt() === ':' && /^:(?:leveloffset: .*|!leveloffset:|leveloffset!:)$/.test(line)) {
349
+ if (
350
+ line.charAt() === ':' &&
351
+ ~line.indexOf(':', 2) &&
352
+ (line.match(AttributeEntryRx) || ['', ''])[1].replace('!', '') === 'leveloffset'
353
+ ) {
269
354
  if (lines[idx - 1] === '') lines[idx - 1] = undefined
270
355
  lines[idx] = undefined
271
356
  continue
@@ -276,16 +361,16 @@ function aggregateAsciiDoc (
276
361
  if (!refs['$key?'](refid) && (~refid.indexOf(' ') || refid.toLowerCase() !== refid)) {
277
362
  if ((refid = doc.$resolve_id(refid))['$nil?']()) return m
278
363
  }
279
- return `<<${idprefix}${refid}${text ? ',' + text : ''}>>`
364
+ return `<<${idPrefix}${refid}${text ? ',' + text : ''}>>`
280
365
  })
281
366
  }
282
367
  // NOTE: the next check takes care of inline and block anchors
283
368
  if (~line.indexOf('[[')) {
284
- line = line.replace(/\[\[([\p{Alpha}_:][\p{Alpha}0-9_\-:.]*)(|, *.+?)\]\]/gu, `[[${idprefix}$1$2]]`)
369
+ line = line.replace(/\[\[([\p{Alpha}_:][\p{Alpha}0-9_\-:.]*)(|, *.+?)\]\]/gu, `[[${idPrefix}$1$2]]`)
285
370
  }
286
371
  if (~line.indexOf('xref:')) {
287
372
  // Q: should we allow : as first character of target?
288
- line = line.replace(/(?<![\\+])xref:([\p{Alpha}0-9_/.{#].*?)\[(|.*?[^\\])\]/gu, (m, target, text) => {
373
+ line = line.replace(/(?<![\\+])xref:((?:\.\/)?[\p{Alpha}0-9_/.{#].*?)\[(|.*?[^\\])\]/gu, (m, target, text) => {
289
374
  let pagePart, fragment, targetPage
290
375
  const hashIdx = target.indexOf('#')
291
376
  if (~hashIdx) {
@@ -302,8 +387,8 @@ function aggregateAsciiDoc (
302
387
  if (!pagePart) {
303
388
  // Q: should we validate the internal ID here?
304
389
  return text && ~text.indexOf('=')
305
- ? `xref:${idprefix}${fragment}[${text}]`
306
- : `<<${idprefix}${fragment}${text ? ',' + text.replace(/\\]/g, ']') : ''}>>`
390
+ ? `xref:${idPrefix}${fragment}[${text}]`
391
+ : `<<${idPrefix}${fragment}${text ? ',' + text.replace(/\\]/g, ']') : ''}>>`
307
392
  }
308
393
  if (~pagePart.indexOf('@') || /:.*:/.test(pagePart)) {
309
394
  if (siteUrl && (targetPage = contentCatalog.resolvePage(pagePart, page.src)) && targetPage.out) {
@@ -312,24 +397,30 @@ function aggregateAsciiDoc (
312
397
  }
313
398
  // TODO: handle unresolved page better
314
399
  return m
315
- } else if (pagePart.indexOf(':') < 0) {
316
- if (module_ !== 'ROOT') pagePart = `${module_}:${pagePart}`
317
- } else if (pagePart.startsWith('ROOT:')) {
318
- pagePart = pagePart.slice(5)
319
400
  }
320
- if (!(targetPage = pagesInOutline.get(pagePart))) {
321
- if (siteUrl && (targetPage = contentCatalog.resolvePage(pagePart, page.src)) && targetPage.out) {
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) {
322
413
  text ||= targetPage.asciidoc?.xreftext || target
323
414
  return `${siteUrl}${targetPage.pub.url}${fragment && '#' + fragment}[${text}]`
324
415
  }
325
416
  // TODO: handle unresolved page better
326
417
  return m
327
418
  }
328
- pagePart = pagePart
329
- .replace(/[/]/g, '::')
330
- .replace(/\.adoc$/, '')
331
- .replace(/[.]/g, '-')
332
- const refid = `${pagePart}:::${fragment}`
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 : '')
333
424
  return `<<${refid}${text && text !== targetPage.title ? ',' + text.replace(/\\]/g, ']') : ''}>>`
334
425
  })
335
426
  }
@@ -354,7 +445,7 @@ function aggregateAsciiDoc (
354
445
  const image = contentCatalog.resolveResource(target, page.src, 'image', ['image'])
355
446
  // TODO: handle (or report) unresolved image better
356
447
  if (image?.out) {
357
- image.out.assembled = true
448
+ pagesInOutline.assembled.assets.add(image)
358
449
  return `image:${image.out.path.replace(/_/g, '{underscore}')}[${attrlist}]`
359
450
  }
360
451
  }
@@ -374,21 +465,18 @@ function aggregateAsciiDoc (
374
465
  const context = block.getContext()
375
466
  let idx = lineno - 1
376
467
  if (context === 'section' && !block.getDocument().isNested()) {
377
- if (block.getSectionName() === 'header') {
378
- lines[idx] = undefined
379
- return
380
- }
381
- let blockStyle = sectionMergeStrategy === 'discrete' ? 'discrete' : undefined
468
+ if (block.getSectionName() === 'header') return
469
+ let blockStyle = (assemblyModel.sectionMergeStrategy || 'discrete') === 'discrete' ? 'discrete' : undefined
382
470
  lines[idx] = lines[idx].replace(/^=+ (.+)/, (_, rest) => {
383
471
  let targetMarkerLength = block.level + 1 + level + (enclosed ? 1 : 0)
384
472
  if (targetMarkerLength > 6) {
473
+ blockStyle = `discrete.h${targetMarkerLength}`
385
474
  targetMarkerLength = 6
386
- blockStyle = 'discrete'
387
475
  }
388
476
  return '='.repeat(targetMarkerLength) + ' ' + rest
389
477
  })
390
478
  // NOTE: ID will be undefined if sectids are turned off
391
- if (block.getId()) rewriteStyleAttribute(block, lines, idx, idprefix, blockStyle)
479
+ if (block.getId()) rewriteStyleAttribute(block, lines, idx, idPrefix, blockStyle)
392
480
  } else {
393
481
  if (context === 'image') {
394
482
  let line = lines[idx] || ''
@@ -414,7 +502,7 @@ function aggregateAsciiDoc (
414
502
  // FIXME: handle (or report) case when image is not resolved
415
503
  if (image?.out) {
416
504
  const boxedAttrlist = line.slice(line.indexOf('['))
417
- image.out.assembled = true
505
+ pagesInOutline.assembled.assets.add(image)
418
506
  lines[idx] = `${prefix}image::${image.out.path}${boxedAttrlist}`
419
507
  }
420
508
  }
@@ -424,11 +512,14 @@ function aggregateAsciiDoc (
424
512
  // nested document
425
513
  idx = (block.getHeader().getLineNumber() || idx + 1) - 1
426
514
  }
427
- if (block.getId()) rewriteStyleAttribute(block, lines, idx, idprefix)
515
+ if (block.getId()) rewriteStyleAttribute(block, lines, idx, idPrefix)
428
516
  }
429
517
  })
430
- buffer.push(...lines.filter((it) => it !== undefined))
431
- const attributeEntries = Object.entries(doc.attributes_defined_in_header || {})
518
+ safePush(
519
+ buffer,
520
+ lines.filter((it) => it !== undefined)
521
+ )
522
+ const attributeEntries = Object.entries(doc.source_header_attributes?.$$smap || {})
432
523
  if (attributeEntries.length) {
433
524
  const resolvedAttributeEntries = attributeEntries.reduce(
434
525
  (accum, [name, val]) => {
@@ -440,27 +531,20 @@ function aggregateAsciiDoc (
440
531
  } else if (val !== initialVal) {
441
532
  accum.push(`:${name}:${initialVal ? ' ' + initialVal : ''}`)
442
533
  }
443
- } else if (
444
- !(
445
- val == null ||
446
- doc.isAttributeLocked(name) ||
447
- name === 'doctype' ||
448
- name === 'leveloffset' ||
449
- name === 'underscore'
450
- )
451
- ) {
534
+ } else if (!(val == null || doc.isAttributeLocked(name) || DiscardAttributes.includes(name))) {
452
535
  accum.push(`:!${name}:`)
453
536
  }
454
537
  return accum
455
538
  },
456
539
  ['']
457
540
  )
458
- if (resolvedAttributeEntries.length > 1) buffer.push(...resolvedAttributeEntries)
541
+ if (resolvedAttributeEntries.length > 1) safePush(buffer, resolvedAttributeEntries)
459
542
  }
460
543
  } else if (level) {
461
- if (level === 1 && navtitlePlain === componentVersion.title) {
544
+ if (atDocumentRoot && navtitlePlain === componentVersion.title) {
462
545
  level--
463
546
  } else {
547
+ buffer.inBody = true
464
548
  buffer.push('')
465
549
  // NOTE: try to toggle sectids; otherwise, fallback to globally unique synthetic ID
466
550
  // Q: should we unset docname, page-module, etc?
@@ -480,7 +564,7 @@ function aggregateAsciiDoc (
480
564
  if (urlType === 'external') {
481
565
  sectionTitle = `${url}[${navtitleAsciiDoc.replace(/\]/g, '\\]')}]`
482
566
  } else if (urlType === 'internal' && !unresolved && siteUrl) {
483
- const resource = contentCatalog.getFiles().find((it) => it.out && it.pub.url === url)
567
+ const resource = files.find((it) => it.pub.url === url)
484
568
  if (resource) sectionTitle = `${siteUrl}${resource.pub.url}[${navtitleAsciiDoc.replace(/\]/g, '\\]')}]`
485
569
  }
486
570
  let hlevel = level + 1
@@ -495,50 +579,112 @@ function aggregateAsciiDoc (
495
579
  }
496
580
  }
497
581
 
498
- const nextLevel = level + 1
499
582
  if (hasItems) {
583
+ const nextLevel = level + 1
500
584
  // NOTE: drop first child if same as parent; should we keep if content is different?
501
585
  ;(urlType === 'internal' && urlType === items[0].urlType && url === items[0].url && !items[0].items
502
586
  ? items.slice(1)
503
587
  : items
504
588
  ).forEach((item) => {
505
- buffer.push(
506
- ...aggregateAsciiDoc(
507
- loadAsciiDoc,
508
- contentCatalog,
509
- header,
510
- componentVersion,
511
- item,
512
- pagesInOutline,
513
- asciidocConfig,
514
- mutableAttributes,
515
- sectionMergeStrategy,
516
- lastComponentVersion,
517
- nextLevel
518
- )
589
+ mergeAsciiDoc(
590
+ loadAsciiDoc,
591
+ contentCatalog,
592
+ buffer,
593
+ componentVersion,
594
+ item,
595
+ files,
596
+ pagesInOutline,
597
+ asciidocConfig,
598
+ mutableAttributes,
599
+ assemblyModel,
600
+ lastComponentVersion,
601
+ nextLevel,
602
+ atBookRoot
519
603
  )
520
604
  })
521
605
  }
522
606
  return buffer
523
607
  }
524
608
 
609
+ function processDocumentHeader (doc, lines, buffer, ignoreLines) {
610
+ const doctitleIdx = doc.getHeader().getLineNumber() - 1
611
+ const end = doc.getBlocks()[0]?.getLineNumber() ?? lines.length
612
+ let belowDoctitle
613
+ let open
614
+ const implicitLines = []
615
+ for (let idx = 0; idx < end; idx++) {
616
+ if (idx === doctitleIdx) {
617
+ lines[idx] = undefined
618
+ ignoreLines.push(idx)
619
+ belowDoctitle = true
620
+ continue
621
+ }
622
+ const line = lines[idx]
623
+ if (open === ':' || open === '-:') {
624
+ if (line) {
625
+ if (open === ':') buffer.push(line)
626
+ if (!line.endsWith(' \\')) open = undefined
627
+ } else {
628
+ open = undefined
629
+ }
630
+ } else if (line) {
631
+ const chr0 = line.charAt()
632
+ let attributeEntryMatch
633
+ 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) === '/') {
637
+ break
638
+ }
639
+ buffer.push(line)
640
+ } 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 = '-:'
646
+ } else {
647
+ if (line.endsWith(' \\')) open = ':'
648
+ buffer.push(line)
649
+ }
650
+ } else if (belowDoctitle) {
651
+ if (implicitLines.length === 2 || !/[\p{Alpha}0-9]/u.test(chr0)) break
652
+ implicitLines.push(line)
653
+ }
654
+ } else if (belowDoctitle) {
655
+ break
656
+ }
657
+ lines[idx] = undefined
658
+ ignoreLines.push(idx)
659
+ }
660
+ return doc
661
+ .getRoles()
662
+ .map((role) => '.' + role)
663
+ .join('')
664
+ }
665
+
525
666
  function generateSlug (title) {
526
667
  return title
527
668
  .toLowerCase()
528
- .replace(/&.+?;|[^ \p{Alpha}0-9_\-.]/gu, '')
529
- .replace(/[ _.]/g, '-')
530
- .replace(/--+/g, '-')
669
+ .replace(/<[^>]+>/g, '')
670
+ .replace(CharRefRx, (_, name, dec, hex) => {
671
+ if (name) return BuiltInNamedEntities[name] ?? '?'
672
+ return String.fromCharCode(dec ? parseInt(dec, 10) : parseInt(hex, 16))
673
+ })
674
+ .replace(/[\x27\u2019]/g, '')
675
+ .replace(/[^\p{Alpha}0-9\-]/gu, '-')
676
+ .replace(/^-+|-+$|(-)-+/g, '$1')
531
677
  }
532
678
 
533
- function fixSectionLevels (sections, multipart) {
679
+ function fixSectionLevels (sections, expectedLevel) {
534
680
  sections.forEach((sect) => {
535
- const targetLevel = sect.getParent().getLevel() + 1
536
- if (multipart ? sect.getLevel() > targetLevel : sect.getLevel() !== targetLevel) sect.level = targetLevel
537
- if (sect.hasSections()) fixSectionLevels(sect.getSections(), multipart)
681
+ const forceLevel = expectedLevel ?? Math.min(1, sect.getLevel())
682
+ if (sect.getLevel() !== forceLevel) sect.level = forceLevel
683
+ if (sect.hasSections()) fixSectionLevels(sect.getSections(), forceLevel + 1)
538
684
  })
539
685
  }
540
686
 
541
- function rewriteStyleAttribute (block, lines, idx, idprefix, replacementStyle = '') {
687
+ function rewriteStyleAttribute (block, lines, idx, idPrefix, replacementStyle = '') {
542
688
  let prevLine = lines[idx - 1]
543
689
  const char0 = prevLine?.charAt()
544
690
  if (char0) {
@@ -548,7 +694,7 @@ function rewriteStyleAttribute (block, lines, idx, idprefix, replacementStyle =
548
694
  prevLine.charAt(1) === '[' &&
549
695
  /^\[\[(?:|[\p{Alpha}_:][\p{Alpha}0-9_\-:.]*(?:, *.+)?)\]\]$/u.test(prevLine))
550
696
  ) {
551
- return rewriteStyleAttribute(block, lines, idx - 1, idprefix, replacementStyle)
697
+ return rewriteStyleAttribute(block, lines, idx - 1, idPrefix, replacementStyle)
552
698
  }
553
699
  }
554
700
  let cellSpec
@@ -564,27 +710,27 @@ function rewriteStyleAttribute (block, lines, idx, idprefix, replacementStyle =
564
710
  let rawStyle
565
711
  const commaIdx = prevLine.indexOf(',')
566
712
  if (~commaIdx) {
567
- rawStyle = prevLine.slice(1, commaIdx - 1)
713
+ rawStyle = prevLine.slice(1, commaIdx)
568
714
  if (~rawStyle.indexOf('=')) rawStyle = undefined
569
715
  } else if (!~prevLine.indexOf('=')) {
570
- rawStyle = prevLine.slice(1, prevLine.length - 2)
716
+ rawStyle = prevLine.slice(1, prevLine.length - 1)
571
717
  }
572
718
  if (rawStyle) {
573
719
  if (~rawStyle.indexOf('#')) {
574
- prevLine = prevLine.replace(/#[^.%,\]]+/, `#${idprefix}${block.getId()}`)
720
+ prevLine = prevLine.replace(/#[^.%,\]]+/, `#${idPrefix}${block.getId()}`)
575
721
  if (replacementStyle) prevLine = prevLine.replace(/^[^#.%,\]]+/, `[${replacementStyle}`)
576
722
  } else {
577
723
  prevLine = `[${
578
724
  replacementStyle ? rawStyle.replace(/^[^.%]*/, replacementStyle) : rawStyle
579
- }#${idprefix}${block.getId()}${prevLine.slice(rawStyle.length + 1)}`
725
+ }#${idPrefix}${block.getId()}${prevLine.slice(rawStyle.length + 1)}`
580
726
  }
581
727
  } else {
582
- prevLine = `[${replacementStyle}#${idprefix}${block.getId()}${rawStyle == null ? ',' : ''}${prevLine.slice(1)}`
728
+ prevLine = `[${replacementStyle}#${idPrefix}${block.getId()}${rawStyle == null ? ',' : ''}${prevLine.slice(1)}`
583
729
  }
584
730
  if (cellSpec) prevLine = `${cellSpec}|${prevLine}`
585
731
  lines[idx - 1] = prevLine
586
732
  } else {
587
- lines.splice(idx, 0, `[${replacementStyle}#${idprefix}${block.getId()}]`)
733
+ lines.splice(idx, 0, `[${replacementStyle}#${idPrefix}${block.getId()}]`)
588
734
  }
589
735
  }
590
736
 
@@ -596,4 +742,14 @@ function getObjectId (obj) {
596
742
  return global.Opal.uid()
597
743
  }
598
744
 
599
- module.exports = produceAggregateDocument
745
+ function safePush (onto, entries) {
746
+ try {
747
+ onto.push(...entries)
748
+ } catch (err) {
749
+ /* istanbul ignore if */
750
+ if (!(err instanceof RangeError)) throw err
751
+ for (const e of entries) onto.push(e)
752
+ }
753
+ }
754
+
755
+ module.exports = produceAssemblyFile