@antora/assembler 1.0.0-alpha.8 → 1.0.0-beta.1

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,70 +1,72 @@
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
+ const unconvertInlineAsciiDoc = require('./util/unconvert-inline-asciidoc')
6
7
 
7
- 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 (
8
15
  loadAsciiDoc,
9
16
  contentCatalog,
10
17
  componentVersion,
11
18
  outline,
12
- doctype,
13
- pages,
19
+ files,
14
20
  asciidocConfig,
15
21
  mutableAttributes,
16
- sectionMergeStrategy = 'discrete'
22
+ assemblyModel
17
23
  ) {
18
- 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)
19
27
  const navtitle = outline.content
20
- const templateFile = contentCatalog.addFile({
21
- src: {
22
- component: componentVersion.name,
23
- version: componentVersion.version,
24
- module: 'ROOT',
25
- family: 'page',
26
- relative: generateSlug(navtitle),
27
- },
28
- })
29
- const path_ = `${templateFile.out.path}.adoc`
30
- contentCatalog.removeFile(templateFile)
31
- const header = buildAsciiDocHeader(componentVersion, navtitle, doctype)
32
- const body = aggregateAsciiDoc(
28
+ const buffer = mergeAsciiDoc(
33
29
  loadAsciiDoc,
34
30
  contentCatalog,
35
- header,
31
+ buildAsciiDocHeader(componentVersion, navtitle, assemblyModel),
36
32
  componentVersion,
37
33
  outline,
34
+ files,
38
35
  pagesInOutline,
39
36
  asciidocConfig,
40
37
  mutableAttributes,
41
- sectionMergeStrategy
38
+ assemblyModel
42
39
  )
43
- return new File({
44
- 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, {
45
46
  asciidoc: asciidocConfig,
46
- contents: Buffer.from([...header, ...body].join('\n') + '\n'),
47
- mediaType: 'text/asciidoc',
48
- path: path_,
47
+ assembler: { assembled: pagesInOutline.assembled, downloadStem, rootLevel },
48
+ contents: Buffer.from(buffer.join('\n') + '\n'),
49
49
  src: {
50
50
  component: componentVersion.name,
51
51
  version: componentVersion.version,
52
- basename: path.basename(path_),
53
- stem: path.basename(path_, '.adoc'),
54
- extname: '.adoc',
52
+ module: 'ROOT',
53
+ family: 'export',
54
+ relative: stem + '.adoc',
55
55
  },
56
56
  })
57
57
  }
58
58
 
59
- function buildAsciiDocHeader (componentVersion, navtitle, doctype = 'book') {
60
- const [navtitlePlain, navtitleAsciiDoc] = sanitize(navtitle)
59
+ function buildAsciiDocHeader (componentVersion, navtitle, assemblyModel) {
60
+ const doctype = assemblyModel.doctype ?? 'book'
61
+ const navtitlePlain = sanitize(navtitle)
62
+ const navtitleAsciiDoc = unconvertInlineAsciiDoc(navtitle)
61
63
  let doctitle = navtitleAsciiDoc
62
64
  if (navtitlePlain !== componentVersion.title) doctitle = `${componentVersion.title}: ${doctitle}`
63
65
  const version = componentVersion.version === 'master' ? '' : componentVersion.version
64
66
  return [
65
67
  `= ${doctitle}`,
66
68
  ...(version ? [`:revnumber: ${version}`] : []),
67
- ...(doctype === 'article' ? [] : [`:doctype: ${doctype}`]),
69
+ ...(doctype === 'article' ? [] : [`:doctype: ${doctype ?? 'book'}`]),
68
70
  ':underscore: _',
69
71
  // Q: should we pass these via the CLI so they cannot be modified?
70
72
  `:page-component-name: ${componentVersion.name}`,
@@ -75,44 +77,52 @@ function buildAsciiDocHeader (componentVersion, navtitle, doctype = 'book') {
75
77
  ]
76
78
  }
77
79
 
78
- function selectPagesInOutline (outlineEntry, pages) {
79
- const page = outlineEntry.urlType === 'internal' ? pages.find((it) => it.pub.url === outlineEntry.url) : undefined
80
- return (outlineEntry.items || []).reduce(
81
- (accum, item) => new Map([...accum, ...selectPagesInOutline(item, pages)]),
82
- new Map(
83
- page && [
84
- [`${page.src.module === 'ROOT' ? '' : page.src.module + ':'}${page.src.relative}`, page],
85
- [page.pub.url, page],
86
- ]
87
- )
88
- )
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
89
91
  }
90
92
 
91
- function aggregateAsciiDoc (
93
+ function mergeAsciiDoc (
92
94
  loadAsciiDoc,
93
95
  contentCatalog,
94
- header,
96
+ buffer,
95
97
  componentVersion,
96
98
  outlineEntry,
99
+ files,
97
100
  pagesInOutline,
98
101
  asciidocConfig,
99
102
  mutableAttributes,
100
- sectionMergeStrategy,
103
+ assemblyModel,
101
104
  lastComponentVersion = componentVersion,
102
- level = 0
105
+ level = 0,
106
+ supportsParts = false
103
107
  ) {
104
- const buffer = []
105
108
  // TODO: we could try to be smart about it and make sure the page with fragment is included at least once
106
109
  if (outlineEntry.hash) return buffer
107
- const { content: navtitle, items, unresolved, urlType, url } = outlineEntry
108
- const [navtitlePlain, navtitleAsciiDoc, navtitlePass] = sanitize(navtitle)
110
+ const { content: navtitle, items = [], unresolved, urlType, url } = outlineEntry
111
+ const atDocumentRoot = !buffer.inBody
112
+ const atBookRoot = atDocumentRoot && !level && assemblyModel.doctype === 'book' && (supportsParts = true)
113
+ const hasItems = items.length > 0
114
+ const navtitlePlain = sanitize(navtitle)
115
+ const navtitleAsciiDoc = unconvertInlineAsciiDoc(navtitle)
109
116
  const siteUrl = ((val) => {
110
117
  if (!val || val === '/') return ''
111
118
  return val.charAt(val.length - 1) === '/' ? val.slice(0, val.length - 1) : val
112
- })(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
113
123
  // FIXME: ideally, resource ID would be stored in navigation so we can look up the page more efficiently
114
124
  let page = urlType === 'internal' && !unresolved ? pagesInOutline.get(url) : undefined
115
- if (page && pagesInOutline.aggregated?.includes(page)) page = undefined
125
+ if (page && pagesInOutline.assembled.pages.has(page)) page = undefined
116
126
  if (page) {
117
127
  let contents = page.src.contents
118
128
  if (contents == null) return buffer
@@ -125,20 +135,51 @@ function aggregateAsciiDoc (
125
135
  .replace(/^(?:[ \t]*\r\n?|[ \t]*\n)+/, '')
126
136
  .trimRight()
127
137
  )
128
- ;(pagesInOutline.aggregated ??= []).push(page)
129
- page = new page.constructor(Object.assign({}, page, { contents, mediaType: 'text/asciidoc' }))
130
138
  const { component, version, module: module_, relative, origin } = page.src
131
- const doc = loadAsciiDoc(page, contentCatalog, asciidocConfig)
132
- 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
+ }
133
154
  // NOTE: in Antora, docname is relative src path from module without file extension
134
155
  const docname = doc.getAttribute('docname')
135
- const docnameForId = docname.replace(/[/]/g, '::').replace(/[.]/g, '-')
136
- const scopeId = component !== componentVersion.name
137
- const idprefix =
138
- (scopeId ? component + ':' : '') +
139
- (module_ === 'ROOT' ? (scopeId ? ':' : '') : module_ + ':') +
140
- docnameForId +
141
- ':::'
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
142
183
  buffer.push('')
143
184
  buffer.push(`:docname: ${docname}`)
144
185
  if (component !== lastComponentVersion.name) {
@@ -163,25 +204,50 @@ function aggregateAsciiDoc (
163
204
  buffer.push(`:page-origin-refname: ${origin.branch || origin.tag}`)
164
205
  buffer.push(`:page-origin-reftype: ${origin.branch ? 'branch' : 'tag'}`)
165
206
  buffer.push(`:page-origin-refhash: ${origin.worktree ? '(worktree)' : origin.refhash}`)
166
- let enclosed
167
- // NOTE: if level is 0, doctitle has already been added and we're in the document header
168
- if (level) {
169
- if (level === 1 && navtitlePlain === componentVersion.title) {
170
- 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' })
171
222
  } else {
172
- let hlevel = level + 1
173
- if (hlevel > 6) {
174
- hlevel = 6
175
- buffer.push(`[discrete#${idprefix}]`)
223
+ if (atDocumentRoot) {
224
+ pageFragment = `#${idPrefix}${doc.getId() ?? pageStyle}`
225
+ buffer.unshift(`[#${idScope}]`)
176
226
  } else {
177
- 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}` })
178
240
  }
179
- buffer.push(`${'='.repeat(hlevel)} ${navtitleAsciiDoc}`)
180
241
  }
181
- } else {
182
- header.unshift(`[#${idprefix}]`)
183
242
  }
184
- if (sectionMergeStrategy === 'enclose' && items && 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()) {
185
251
  enclosed = true
186
252
  // TODO: make overview section title configurable
187
253
  //let overviewTitle = doc.getDocumentTitle()
@@ -200,23 +266,43 @@ function aggregateAsciiDoc (
200
266
  }
201
267
  let hlevel = level + 2
202
268
  if (hlevel > 6) {
269
+ const blockStyle = `discrete.h${hlevel}`
203
270
  hlevel = 6
204
- buffer.push(syntheticId ? `[discrete#${syntheticId}]` : '[discrete]')
271
+ buffer.push(syntheticId ? `[${blockStyle}#${syntheticId}]` : `[${blockStyle}]`)
205
272
  } else if (syntheticId) {
206
273
  buffer.push(`[#${syntheticId}]`)
207
274
  }
208
275
  buffer.push(`${'='.repeat(hlevel)} ${overviewTitle}`)
209
276
  if (toggleSectids) buffer.push(':sectids:')
210
277
  }
211
- const lines = doc.getSourceLines()
212
- const ignoreLines = []
213
- // TODO: think more about when multipart is allowed; perhaps configurable
214
- 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
+ }
215
282
  const allBlocks = doc.findBy({ traverse_documents: true }, (it) =>
216
283
  it.getContext() === 'document'
217
284
  ? it.getDocument().isNested()
218
285
  : !(it.getContext() === 'table_cell' && it.getStyle() === 'asciidoc')
219
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
220
306
  allBlocks.forEach((block) => {
221
307
  const contentModel = block.content_model
222
308
  if (
@@ -244,7 +330,7 @@ function aggregateAsciiDoc (
244
330
  }
245
331
  })
246
332
  let skipping
247
- for (let idx = 0, len = lines.length; idx < len; idx++) {
333
+ for (let idx = 0, lastIdx = lines.length - 1; idx <= lastIdx; idx++) {
248
334
  if (~ignoreLines.indexOf(idx)) continue
249
335
  let line = lines[idx]
250
336
  if (line.startsWith('//')) {
@@ -260,7 +346,11 @@ function aggregateAsciiDoc (
260
346
  } else if (skipping) {
261
347
  continue
262
348
  }
263
- 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
+ ) {
264
354
  if (lines[idx - 1] === '') lines[idx - 1] = undefined
265
355
  lines[idx] = undefined
266
356
  continue
@@ -271,16 +361,16 @@ function aggregateAsciiDoc (
271
361
  if (!refs['$key?'](refid) && (~refid.indexOf(' ') || refid.toLowerCase() !== refid)) {
272
362
  if ((refid = doc.$resolve_id(refid))['$nil?']()) return m
273
363
  }
274
- return `<<${idprefix}${refid}${text ? ',' + text : ''}>>`
364
+ return `<<${idPrefix}${refid}${text ? ',' + text : ''}>>`
275
365
  })
276
366
  }
277
367
  // NOTE: the next check takes care of inline and block anchors
278
368
  if (~line.indexOf('[[')) {
279
- 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]]`)
280
370
  }
281
371
  if (~line.indexOf('xref:')) {
282
372
  // Q: should we allow : as first character of target?
283
- 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) => {
284
374
  let pagePart, fragment, targetPage
285
375
  const hashIdx = target.indexOf('#')
286
376
  if (~hashIdx) {
@@ -297,8 +387,8 @@ function aggregateAsciiDoc (
297
387
  if (!pagePart) {
298
388
  // Q: should we validate the internal ID here?
299
389
  return text && ~text.indexOf('=')
300
- ? `xref:${idprefix}${fragment}[${text}]`
301
- : `<<${idprefix}${fragment}${text ? ',' + text.replace(/\\]/g, ']') : ''}>>`
390
+ ? `xref:${idPrefix}${fragment}[${text}]`
391
+ : `<<${idPrefix}${fragment}${text ? ',' + text.replace(/\\]/g, ']') : ''}>>`
302
392
  }
303
393
  if (~pagePart.indexOf('@') || /:.*:/.test(pagePart)) {
304
394
  if (siteUrl && (targetPage = contentCatalog.resolvePage(pagePart, page.src)) && targetPage.out) {
@@ -307,24 +397,30 @@ function aggregateAsciiDoc (
307
397
  }
308
398
  // TODO: handle unresolved page better
309
399
  return m
310
- } else if (pagePart.indexOf(':') < 0) {
311
- if (module_ !== 'ROOT') pagePart = `${module_}:${pagePart}`
312
- } else if (pagePart.startsWith('ROOT:')) {
313
- pagePart = pagePart.slice(5)
314
400
  }
315
- if (!(targetPage = pagesInOutline.get(pagePart))) {
316
- 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) {
317
413
  text ||= targetPage.asciidoc?.xreftext || target
318
414
  return `${siteUrl}${targetPage.pub.url}${fragment && '#' + fragment}[${text}]`
319
415
  }
320
416
  // TODO: handle unresolved page better
321
417
  return m
322
418
  }
323
- pagePart = pagePart
324
- .replace(/[/]/g, '::')
325
- .replace(/\.adoc$/, '')
326
- .replace(/[.]/g, '-')
327
- 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 : '')
328
424
  return `<<${refid}${text && text !== targetPage.title ? ',' + text.replace(/\\]/g, ']') : ''}>>`
329
425
  })
330
426
  }
@@ -340,9 +436,7 @@ function aggregateAsciiDoc (
340
436
  relative,
341
437
  })
342
438
  // TODO: handle unresolved attachment page
343
- return attachment && attachment.out
344
- ? `${siteUrl}${attachment.pub.url.replace(/_/g, '{underscore}')}[${text}]`
345
- : m
439
+ return attachment?.out ? `${siteUrl}${attachment.pub.url.replace(/_/g, '{underscore}')}[${text}]` : m
346
440
  })
347
441
  }
348
442
  if (~line.indexOf('image:') && !line.startsWith('image::')) {
@@ -350,8 +444,8 @@ function aggregateAsciiDoc (
350
444
  if (isResourceSpec(target)) {
351
445
  const image = contentCatalog.resolveResource(target, page.src, 'image', ['image'])
352
446
  // TODO: handle (or report) unresolved image better
353
- if (image && image.out) {
354
- image.out.assembled = true
447
+ if (image?.out) {
448
+ pagesInOutline.assembled.assets.add(image)
355
449
  return `image:${image.out.path.replace(/_/g, '{underscore}')}[${attrlist}]`
356
450
  }
357
451
  }
@@ -371,21 +465,18 @@ function aggregateAsciiDoc (
371
465
  const context = block.getContext()
372
466
  let idx = lineno - 1
373
467
  if (context === 'section' && !block.getDocument().isNested()) {
374
- if (block.getSectionName() === 'header') {
375
- lines[idx] = undefined
376
- return
377
- }
378
- let blockStyle = sectionMergeStrategy === 'discrete' ? 'discrete' : undefined
468
+ if (block.getSectionName() === 'header') return
469
+ let blockStyle = (assemblyModel.sectionMergeStrategy || 'discrete') === 'discrete' ? 'discrete' : undefined
379
470
  lines[idx] = lines[idx].replace(/^=+ (.+)/, (_, rest) => {
380
471
  let targetMarkerLength = block.level + 1 + level + (enclosed ? 1 : 0)
381
472
  if (targetMarkerLength > 6) {
473
+ blockStyle = `discrete.h${targetMarkerLength}`
382
474
  targetMarkerLength = 6
383
- blockStyle = 'discrete'
384
475
  }
385
476
  return '='.repeat(targetMarkerLength) + ' ' + rest
386
477
  })
387
478
  // NOTE: ID will be undefined if sectids are turned off
388
- if (block.getId()) rewriteStyleAttribute(block, lines, idx, idprefix, blockStyle)
479
+ if (block.getId()) rewriteStyleAttribute(block, lines, idx, idPrefix, blockStyle)
389
480
  } else {
390
481
  if (context === 'image') {
391
482
  let line = lines[idx] || ''
@@ -409,9 +500,9 @@ function aggregateAsciiDoc (
409
500
  if (isResourceSpec(target)) {
410
501
  const image = contentCatalog.resolveResource(target, page.src, 'image', ['image'])
411
502
  // FIXME: handle (or report) case when image is not resolved
412
- if (image && image.out) {
503
+ if (image?.out) {
413
504
  const boxedAttrlist = line.slice(line.indexOf('['))
414
- image.out.assembled = true
505
+ pagesInOutline.assembled.assets.add(image)
415
506
  lines[idx] = `${prefix}image::${image.out.path}${boxedAttrlist}`
416
507
  }
417
508
  }
@@ -421,11 +512,14 @@ function aggregateAsciiDoc (
421
512
  // nested document
422
513
  idx = (block.getHeader().getLineNumber() || idx + 1) - 1
423
514
  }
424
- if (block.getId()) rewriteStyleAttribute(block, lines, idx, idprefix)
515
+ if (block.getId()) rewriteStyleAttribute(block, lines, idx, idPrefix)
425
516
  }
426
517
  })
427
- buffer.push(...lines.filter((it) => it !== undefined))
428
- 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 || {})
429
523
  if (attributeEntries.length) {
430
524
  const resolvedAttributeEntries = attributeEntries.reduce(
431
525
  (accum, [name, val]) => {
@@ -437,27 +531,20 @@ function aggregateAsciiDoc (
437
531
  } else if (val !== initialVal) {
438
532
  accum.push(`:${name}:${initialVal ? ' ' + initialVal : ''}`)
439
533
  }
440
- } else if (
441
- !(
442
- val == null ||
443
- doc.isAttributeLocked(name) ||
444
- name === 'doctype' ||
445
- name === 'leveloffset' ||
446
- name === 'underscore'
447
- )
448
- ) {
534
+ } else if (!(val == null || doc.isAttributeLocked(name) || DiscardAttributes.includes(name))) {
449
535
  accum.push(`:!${name}:`)
450
536
  }
451
537
  return accum
452
538
  },
453
539
  ['']
454
540
  )
455
- if (resolvedAttributeEntries.length > 1) buffer.push(...resolvedAttributeEntries)
541
+ if (resolvedAttributeEntries.length > 1) safePush(buffer, resolvedAttributeEntries)
456
542
  }
457
543
  } else if (level) {
458
- if (level === 1 && navtitlePlain === componentVersion.title) {
544
+ if (atDocumentRoot && navtitlePlain === componentVersion.title) {
459
545
  level--
460
546
  } else {
547
+ buffer.inBody = true
461
548
  buffer.push('')
462
549
  // NOTE: try to toggle sectids; otherwise, fallback to globally unique synthetic ID
463
550
  // Q: should we unset docname, page-module, etc?
@@ -475,13 +562,10 @@ function aggregateAsciiDoc (
475
562
  }
476
563
  let sectionTitle = navtitleAsciiDoc
477
564
  if (urlType === 'external') {
478
- sectionTitle = `${url}[${navtitlePass ? navtitleAsciiDoc : navtitleAsciiDoc.replace(/\]/g, '\\]')}]`
565
+ sectionTitle = `${url}[${navtitleAsciiDoc.replace(/\]/g, '\\]')}]`
479
566
  } else if (urlType === 'internal' && !unresolved && siteUrl) {
480
- const resource = contentCatalog.getFiles().find((it) => it.pub.url === url)
481
- if (resource?.out) {
482
- sectionTitle = navtitlePass ? navtitleAsciiDoc : navtitleAsciiDoc.replace(/\]/g, '\\]')
483
- sectionTitle = `${siteUrl}${resource.pub.url}[${sectionTitle}]`
484
- }
567
+ const resource = files.find((it) => it.pub.url === url)
568
+ if (resource) sectionTitle = `${siteUrl}${resource.pub.url}[${navtitleAsciiDoc.replace(/\]/g, '\\]')}]`
485
569
  }
486
570
  let hlevel = level + 1
487
571
  if (hlevel > 6) {
@@ -495,50 +579,112 @@ function aggregateAsciiDoc (
495
579
  }
496
580
  }
497
581
 
498
- const nextLevel = level + 1
499
- if (items) {
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