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

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