@antora/assembler
Version:
A JavaScript library that merges AsciiDoc content from multiple pages in an Antora site into assembly files and delegates to an exporter to convert those files to another format, such as PDF.
792 lines (776 loc) • 31.1 kB
JavaScript
const createAsciiDocFile = require('./util/create-asciidoc-file')
const createResourceKey = require('./util/create-resource-key')
const generateScopedId = require('./util/generate-scoped-id')
const { resolveLinkTarget } = require('./util/resolver')
const {
rewriteXrefs,
rewriteImageAttr,
rewriteImageRef,
rewriteInlineImages,
rewriteStyleAttribute,
} = require('./util/rewriter')
const sanitize = require('./util/sanitize')
const unconvertInlineAsciiDoc = require('./util/unconvert-inline-asciidoc')
const ATTR_ENTRY_RX = /^:(!?[\p{Alpha}0-9_][^:]*):(?: |$)/u
const BUILT_IN_NAMED_ENTITIES = { amp: '&', apos: "'", gt: '>', lt: '<', nbsp: ' ', quot: '"' }
const CHAR_REF_RX = /&(?:([a-z][a-z]+\d{0,2})|#(?:(\d{2,6})|x([a-z\d]{2,5})));/g
const DISCARD_ATTRIBUTE_NAMES = [
'doctype',
'leveloffset',
'assembly-header-attributes',
'assembly-navtitle',
'assembly-slug',
'assembly-style',
'underscore',
]
const { NAMED_ID_ATTR_RX } = require('./util/rx')
function produceAssemblyFile (
loadAsciiDoc,
contentCatalog,
componentVersion,
outline,
files,
asciidocConfig,
mutableAttributes,
assemblyModel
) {
const pagesByUrl = files.reduce((map, it) => (it.src.family === 'page' ? map.set(it.pub.url, it) : map), new Map())
if (outline.urlType === 'internal' && !pagesByUrl.get(outline.url) && !(outline.items || []).length) return
const pagesInOutline = selectPagesInOutline(outline, pagesByUrl)
const { rootLevel, xmlIds } = assemblyModel
const idSeparators = {
prefix:
'assembler-idprefix' in asciidocConfig.attributes ? (asciidocConfig.attributes['assembler-idprefix'] ?? '') : '_',
scope: xmlIds ? '---' : ':::',
coordinate: xmlIds ? '----' : ':',
generateIdFromTitle: generateIdFromTitle.bind(
loadAsciiDoc(
{ contents: Buffer.alloc(0), src: { family: 'page', relative: 'generate-id-from-title.adoc' } },
undefined,
asciidocConfig
)
),
}
const { name: component, version } = componentVersion
asciidocConfig = prepareAsciiDocConfig(
contentCatalog,
{ component, version },
pagesInOutline,
asciidocConfig,
assemblyModel,
idSeparators
)
const buffer = mergeAsciiDoc(
loadAsciiDoc,
contentCatalog,
buildAsciiDocHeader(componentVersion, outline.content, assemblyModel),
componentVersion,
outline,
files,
pagesInOutline,
idSeparators,
asciidocConfig,
mutableAttributes,
assemblyModel
)
const stem = buffer.slug ? sanitizeSlug(buffer.slug) : generateSlug(rootLevel === 0 ? 'index' : buffer.navtitle)
const downloadStem = [component, version, stem === 'index' ? '' : stem].filter((it) => it).join('-')
return createAsciiDocFile(contentCatalog, {
asciidoc: asciidocConfig,
assembler: { assembled: pagesInOutline.assembled, downloadStem, rootLevel },
contents: Buffer.from(buffer.join('\n') + '\n'),
src: { component, version, module: 'ROOT', family: 'export', relative: stem + '.adoc' },
})
}
function prepareAsciiDocConfig (contentCatalog, ctx, pagesInOutline, asciidocConfig, assemblyModel, idSeparators) {
let attributesModified
const configShared = asciidocConfig.$shared
const sharedAttributes = asciidocConfig.attributes
if (!configShared) {
if (configShared == null) {
const assets = pagesInOutline.assembled.assets
for (const [name, val] of Object.entries(sharedAttributes)) {
if (!(typeof val === 'string' && ~val.indexOf(':'))) continue
let newVal
if (!name.endsWith('-image') || !(newVal = rewriteImageAttr(val, contentCatalog, assemblyModel, ctx, assets))) {
if (~(newVal = val).indexOf('image:')) {
newVal = rewriteInlineImages(newVal, contentCatalog, assemblyModel, ctx, assets)
}
}
if (newVal !== val) sharedAttributes[name] = newVal
}
}
for (const [name, val] of Object.entries(sharedAttributes)) {
if (!(typeof val === 'string' && ~val.indexOf(':'))) continue
if (~val.indexOf('xref:')) {
const newVal = rewriteXrefs(val, contentCatalog, assemblyModel, ctx, false, pagesInOutline, idSeparators)
if (newVal !== val) (attributesModified ??= {})[name] = newVal
}
}
if (configShared == null) Object.defineProperty(asciidocConfig, '$shared', { value: attributesModified == null })
}
return Object.assign({}, asciidocConfig, { attributes: Object.assign({}, sharedAttributes, attributesModified) })
}
function buildAsciiDocHeader (componentVersion, navtitle, assemblyModel) {
const doctype = assemblyModel.doctype ?? 'book'
const navtitlePlain = sanitize(navtitle)
const navtitleAsciiDoc = unconvertInlineAsciiDoc(navtitle)
let doctitle = navtitleAsciiDoc
if (navtitlePlain !== componentVersion.title) doctitle = `${componentVersion.title}: ${doctitle}`
const version = componentVersion.version === 'master' ? '' : componentVersion.version
const displayVersion = componentVersion.displayVersion
const buffer = [
`= ${doctitle}`,
...(version ? [`:revnumber: ${displayVersion}`] : []),
...(doctype === 'article' ? [] : [`:doctype: ${doctype ?? 'book'}`]),
':underscore: _',
// Q: should we pass these via the CLI so they cannot be modified?
`:page-component-name: ${componentVersion.name}`,
`:page-component-version:${version ? ' ' + version : ''}`,
':page-version: {page-component-version}',
`:page-component-display-version: ${displayVersion}`,
`:page-component-title: ${componentVersion.title}`,
]
return Object.assign(buffer, { navtitle })
}
function selectPagesInOutline (outlineEntry, pagesByUrl, accum) {
accum ??= Object.assign(new Map(), { assembled: { pages: new Map(), assets: new Set() } })
const page = outlineEntry.urlType === 'internal' ? pagesByUrl.get(outlineEntry.url) : undefined
if (page) {
accum.set(createResourceKey(page.src), page)
accum.set(outlineEntry.url, page)
}
for (const item of outlineEntry.items || []) selectPagesInOutline(item, pagesByUrl, accum)
return accum
}
function mergeAsciiDoc (
loadAsciiDoc,
contentCatalog,
buffer,
componentVersion,
outlineEntry,
files,
pagesInOutline,
idSeparators,
asciidocConfig,
mutableAttributes,
assemblyModel,
lastComponentVersion = componentVersion,
level = 0,
supportsParts = false
) {
// TODO: we could try to be smart about it and make sure the page with fragment is included at least once
if (outlineEntry.hash) {
buffer.inBody ??= false
return buffer
}
let navtitle = outlineEntry.content
let navtitlePlain = sanitize(navtitle)
let navtitleAsciiDoc = unconvertInlineAsciiDoc(navtitle)
const { items = [], unresolved, urlType, url } = outlineEntry
const { doctype, filetype, linkReferenceStyle, pubRoot, siteRoot, logger } = assemblyModel
const assembled = pagesInOutline.assembled
// FIXME: ideally, resource ID would be stored in navigation so we can look up the page more efficiently
const page = urlType === 'internal' && !unresolved ? pagesInOutline.get(url) : undefined
const atDocumentRoot = !buffer.inBody
const isRootPage = page && atDocumentRoot && !level
const atBookRoot = atDocumentRoot && !level && doctype === 'book' && (supportsParts = true)
const hasItems = items.length > 0
if (page && !assembled.pages.has(page)) {
const contents = page.src.contents
if (contents == null) {
buffer.inBody ??= false
return buffer
}
const { component, version, module: module_, relative, origin, mediaType } = page.src
const pageAsAsciiDoc = new page.constructor(
Object.assign({}, page, { contents: trimAsciiDoc(contents), mediaType })
)
const doc = loadAsciiDoc(pageAsAsciiDoc, contentCatalog, asciidocConfig)
doc.logger = logger
? Object.assign(doc.getLogger().$dup(), {
delegate: {
warn () {
return logger.warn.apply(logger, arguments)
},
},
})
: doc.getLogger()
doc.source_header_attributes ??= doc.parent.$to_h()
if (isRootPage && doc.hasAttribute('assembly-slug')) buffer.slug = doc.getAttribute('assembly-slug')
if (doc.hasAttribute('assembly-navtitle')) {
navtitleAsciiDoc = doc.getAttribute('assembly-navtitle')
navtitlePlain = sanitize((navtitle = doc.$apply_reftext_subs(navtitleAsciiDoc)))
if (buffer.inBody == null) {
buffer.navtitle = navtitle
// Q do we need to assert !level here?
if (assemblyModel.rootLevel) {
let doctitle = navtitleAsciiDoc
if (navtitlePlain !== componentVersion.title) doctitle = `${componentVersion.title}: ${doctitle}`
buffer[0] = `= ${doctitle}`
}
}
}
// NOTE: in Antora, docname is relative src path from module without file extension
const docname = doc.getAttribute('docname')
const pageId = generateScopedId(page.src, componentVersion, idSeparators, filetype)
const pageIdLeader = pageId + idSeparators.scope
let pageFragment = ''
let pageRoles = ''
let pageStyle = doc.getAttribute('assembly-style', '')
if (
!pageStyle &&
atBookRoot &&
doc.isAttribute('preface-title') &&
!('assembly-style' in doc.source_header_attributes.$$smap)
) {
pageStyle = 'preface'
}
let part
if ((part = pageStyle === 'part')) {
pageStyle = ''
if (!supportsParts) part = undefined
} else if ((part = pageStyle.endsWith('-part'))) {
pageStyle = pageStyle.slice(0, -5)
if (!supportsParts) part = undefined
}
let nextSectionLevel = 1
const lines = doc.getSourceLines()
const ignoreLines = []
if (!buffer.inBody) {
buffer.inBody = true
buffer.endHeaderIdx = buffer.length - 1
}
buffer.push('')
buffer.push(`:docname: ${docname}`)
if (component !== lastComponentVersion.name) {
const thisComponentVersion =
component === componentVersion.name && version === componentVersion.version
? componentVersion
: contentCatalog.getComponentVersion(component, version)
if (thisComponentVersion) {
buffer.push(`:page-component-name: ${thisComponentVersion.name}`)
buffer.push(`:page-component-version:${thisComponentVersion.version ? ' ' + thisComponentVersion.version : ''}`)
buffer.push(':page-version: {page-component-version}')
buffer.push(`:page-component-display-version: ${thisComponentVersion.displayVersion}`)
buffer.push(`:page-component-title: ${thisComponentVersion.title}`)
lastComponentVersion = thisComponentVersion
}
}
buffer.push(`:page-module: ${module_}`)
buffer.push(`:page-relative-src-path: ${relative}`)
buffer.push(`:page-origin-url: ${origin.url}`)
buffer.push(`:page-origin-start-path:${origin.startPath && ' '}${origin.startPath}`)
buffer.push(`:page-origin-refname: ${origin.branch || origin.tag}`)
buffer.push(`:page-origin-reftype: ${origin.branch ? 'branch' : 'tag'}`)
buffer.push(`:page-origin-refhash: ${origin.worktree ? '(worktree)' : origin.refhash}`)
if (doc.hasHeader()) {
for (const entry of processDocumentHeader(doc, lines, ignoreLines, isRootPage)) {
if (entry.type === 'author_line') {
buffer.splice(1, 0, entry.lines[0])
buffer.endHeaderIdx += 1
} else if (entry.type === 'attribute_entry') {
const name = entry.name
if (!entry.negated && !doc.isAttributeLocked(name)) {
let val, newVal
if (
name.endsWith('-image') &&
(val = doc.getAttribute(name)) &&
(newVal = rewriteImageAttr(val, contentCatalog, assemblyModel, page.src, assembled.assets))
) {
if (newVal !== val) entry.lines = [`:${name}: ${newVal}`]
} else if (~(val = entry.lines.join('\n').slice(name.length + 3)).indexOf(':')) {
newVal = val
if (~newVal.indexOf('image:')) {
newVal = rewriteInlineImages(
newVal,
contentCatalog,
assemblyModel,
page.src,
assembled.assets,
false,
doc
)
}
if (~newVal.indexOf('xref:')) {
newVal = rewriteXrefs(
newVal,
contentCatalog,
assemblyModel,
page.src,
false,
pagesInOutline,
idSeparators,
pageIdLeader,
doc,
{ file: relative, lineno: entry.lineno }
)
}
if (newVal !== val) entry.lines = `:${entry.name}: ${newVal}`.split('\n')
}
}
if (entry.promote) {
buffer.splice(buffer.endHeaderIdx + 1, 0, ...entry.lines)
buffer.endHeaderIdx += entry.lines.length
} else {
buffer.push(...entry.lines)
}
} else {
buffer.push(...entry.lines)
}
}
pageRoles = doc.getRoles().reduce((accum, role) => `${accum}.${role}`, '')
}
let heading
if (pageStyle && (part && level === 1 ? (level = 0) : level) === 0) {
let htitleAsciiDoc = navtitleAsciiDoc
let htitlePlain = navtitlePlain
let htitleOverride
if (
(htitleOverride = pageStyle === 'preface' ? doc.getAttribute('preface-title') : undefined) ||
(atDocumentRoot && (htitleOverride = outlineEntry.navtitle))
) {
htitleAsciiDoc = unconvertInlineAsciiDoc(htitleOverride)
htitlePlain = sanitize(htitleOverride)
}
if (atDocumentRoot && pageStyle === 'preface' && htitlePlain === componentVersion.title) {
assemblyModel = Object.assign({}, assemblyModel, { sectionMergeStrategy: 'discrete' })
} else {
if (atDocumentRoot) {
pageFragment = `#${pageIdLeader}${doc.getId() ?? pageStyle}`
buffer.unshift(`[#${pageId}]`)
} else {
pageFragment = `#${pageId}`
}
heading = { title: htitleAsciiDoc, level: part ? 1 : 2 }
nextSectionLevel++
}
} else if (level) {
if (atDocumentRoot && navtitlePlain === componentVersion.title) {
level--
} else {
pageFragment = `#${pageId}`
if (part && level === 1) level--
if ((heading = { title: navtitleAsciiDoc, level: level + 1 }).level > 6) {
Object.assign(heading, { level: 6, style: `discrete.h${heading.level}` })
}
}
}
if (heading) {
buffer.push(`[${heading.style ?? pageStyle}${pageFragment}${pageRoles}]`)
buffer.push(`${'='.repeat(heading.level)} ${heading.title}`)
} else if (atDocumentRoot) {
buffer.unshift(`[#${pageId}]`)
}
let enclosed
if (assemblyModel.sectionMergeStrategy === 'enclose' && hasItems && doc.hasSections()) {
enclosed = true
// TODO: make overview section title configurable
//let overviewTitle = doc.getDocumentTitle()
//if (overviewTitle === navtitle) overviewTitle = doc.getAttribute('overview-title', 'Overview')
const overviewTitle = doc.getAttribute('overview-title', 'Overview')
buffer.push('')
const syntheticId = idSeparators.generateIdFromTitle(navtitleAsciiDoc, idSeparators)
let hlevel = level + 2
if (hlevel > 6) {
const blockStyle = `discrete.h${hlevel}`
hlevel = 6
buffer.push(`[${blockStyle}#${syntheticId}]`)
} else {
buffer.push(`[#${syntheticId}]`)
}
buffer.push(`${'='.repeat(hlevel)} ${overviewTitle}`)
}
assembled.pages.set(page, pageFragment)
if (doc.hasSections()) {
fixSectionLevels(doc.getSections(), atBookRoot && !pageStyle ? undefined : nextSectionLevel)
}
const allBlocks = doc.findBy({ traverse_documents: true }, (it) =>
it.getContext() === 'document'
? it.getDocument().isNested()
: !(it.getContext() === 'table_cell' && it.getStyle() === 'asciidoc')
)
if (doc.getDoctype() === 'manpage') {
const firstSectionIdx = doc.hasSections() ? doc.getSections()[0].getLineNumber() - 1 : lines.length
for (let idx = 0; idx < firstSectionIdx; idx++) {
if (~ignoreLines.indexOf(idx)) continue
const line = lines[idx]
if (line.startsWith('== ') && line.length > 3) {
allBlocks.unshift({
getContext: () => 'section',
getDocument: () => doc,
getId: () => doc.getAttribute('manname-id'),
getLineNumber: () => idx + 1,
getSectionName: () => undefined,
level: 1,
})
break
}
}
}
const refs = doc.getCatalog().refs
allBlocks.forEach((block) => {
const contentModel = block.content_model
if (
((contentModel === 'verbatim' && block.getContext() !== 'table_cell') ||
contentModel === 'simple' ||
contentModel === 'pass') &&
!block.hasSubstitution('macros')
) {
const lineno = block.getLineNumber()
const idx = typeof lineno === 'number' ? lineno - 1 : undefined
const startLine = lines[idx]
// NOTE: one case this happens if when sourcemap isn't enabled when reducing
if (startLine == null) {
console.log(`null startLine for ${block.getContext()} at ${lineno} in ${relative}`)
return
}
const char0 = startLine.charAt()
// FIXME: needs to be more robust; move logic to helper
const delimited =
startLine.length > 3 &&
startLine === char0.repeat(startLine.length) &&
(char0 === '-' || char0 === '.' || char0 === '+')
// QUESTION: exclude block attribute lines too? what about attribute entries?
for (let i = idx; i < block.lines.length + (delimited ? idx + 2 : idx); i++) ignoreLines.push(i)
}
})
let skipping
for (let idx = 0, lastIdx = lines.length - 1; idx <= lastIdx; idx++) {
if (~ignoreLines.indexOf(idx)) continue
let line = lines[idx]
if (line.startsWith('//')) {
if (line[2] !== '/') continue
if (line.length > 3 && line === '/'.repeat(line.length)) {
if (skipping) {
if (line === skipping) skipping = undefined
} else {
skipping = line
}
continue
}
} else if (skipping) {
continue
}
if (
line.charAt() === ':' &&
~line.indexOf(':', 2) &&
(line.match(ATTR_ENTRY_RX) || ['', ''])[1].replace('!', '') === 'leveloffset'
) {
if (lines[idx - 1] === '') lines[idx - 1] = undefined
lines[idx] = undefined
continue
}
if (~line.indexOf('<<')) {
line = line.replace(/(?<![\\+])<<#?([\p{Alpha}0-9_/.:{][^>,]*?)(?:|, *([^>]+?))?>>/gu, (m, refid, text) => {
// support natural xref
if (!refs['$key?'](refid) && (~refid.indexOf(' ') || refid.toLowerCase() !== refid)) {
if ((refid = doc.$resolve_id(refid))['$nil?']()) return m
}
return `<<${pageIdLeader}${refid}${text ? ',' + text : ''}>>`
})
}
// NOTE: the next check takes care of inline and block anchors
if (~line.indexOf('[[')) {
line = line.replace(/\[\[([\p{Alpha}_:][\p{Alpha}0-9_\-:.]*)(|, *.+?)\]\]/gu, (_, refid, text) => {
return `[[${pageIdLeader}${refid}${text}]]`
})
}
if (~line.indexOf('xref:')) {
line = rewriteXrefs(
line,
contentCatalog,
assemblyModel,
page.src,
true,
pagesInOutline,
idSeparators,
pageIdLeader,
doc,
{ file: relative, lineno: idx + 1 }
)
}
if (~line.indexOf('image:') && !line.startsWith('image::')) {
line = rewriteInlineImages(line, contentCatalog, assemblyModel, page.src, assembled.assets, true, doc)
}
lines[idx] = line
}
// NOTE: need to do this last since it modifies the line numbers
// we could remap the line numbers to make them resilient
// or we could mark which lines to remove and filter them after
let lastImageMacroAt
;[...allBlocks].reverse().forEach((block) => {
const lineno = block.getLineNumber()
// NOTE: lineno is not defined for preamble
if (typeof lineno !== 'number') return
const context = block.getContext()
let idx = lineno - 1
if (context === 'section' && !block.getDocument().isNested()) {
if (block.getSectionName() === 'header') return
let blockStyle = (assemblyModel.sectionMergeStrategy || 'discrete') === 'discrete' ? 'discrete' : undefined
lines[idx] = lines[idx].replace(/^=+ (.+)/, (_, rest) => {
let targetMarkerLength = block.level + 1 + level + (enclosed ? 1 : 0)
if (targetMarkerLength > 6) {
blockStyle = `discrete.h${targetMarkerLength}`
targetMarkerLength = 6
}
return '='.repeat(targetMarkerLength) + ' ' + rest
})
// NOTE: ID will be undefined if sectids are turned off
if (block.getId()) rewriteStyleAttribute(block, lines, idx, pageIdLeader, blockStyle)
} else {
if (context === 'image') {
let line = lines[idx] || ''
let prefix = ''
// Q: can we use startsWith('image::') in certain cases?
let imageMacroOffset = (
lastImageMacroAt?.[0] === idx ? line.slice(0, lastImageMacroAt[1]) : line
).lastIndexOf('image::')
if (imageMacroOffset > 0) {
if (
block.getDocument().isNested() &&
(prefix = line.slice(0, imageMacroOffset)).trimRight().endsWith('|')
) {
line = line.slice(prefix.length)
} else {
imageMacroOffset = -1
}
}
if (~imageMacroOffset) {
const target = block.getAttribute('target')
const newTarget = rewriteImageRef(target, contentCatalog, assemblyModel, page.src, assembled.assets)
if (newTarget) lines[idx] = `${prefix}image::${newTarget}${line.slice(line.indexOf('['))}`
lastImageMacroAt = [idx, imageMacroOffset]
}
} else if (context === 'document' && block.hasHeader()) {
// nested document
idx = (block.getHeader().getLineNumber() || idx + 1) - 1
}
if (block.getId()) rewriteStyleAttribute(block, lines, idx, pageIdLeader)
}
})
safePush(
buffer,
lines.filter((it) => it !== undefined)
)
const attributeEntries = Object.entries(doc.source_header_attributes.$$smap)
if (attributeEntries.length) {
const resolvedAttributeEntries = attributeEntries.reduce(
(accum, [name, val]) => {
// Q: couldn't we just check if attribute is locked?
if (name in mutableAttributes) {
const initialVal = mutableAttributes[name]
if (initialVal == null) {
if (val != null) accum.push(`:!${name}:`)
} else if (val !== initialVal) {
accum.push(`:${name}:${initialVal ? ' ' + initialVal : ''}`)
}
} else if (!(val == null || doc.isAttributeLocked(name) || DISCARD_ATTRIBUTE_NAMES.includes(name))) {
accum.push(`:!${name}:`)
}
return accum
},
['']
)
if (resolvedAttributeEntries.length > 1) safePush(buffer, resolvedAttributeEntries)
}
} else if (level) {
if (atDocumentRoot && navtitlePlain === componentVersion.title) {
buffer.inBody ??= false
level--
} else {
buffer.inBody = true
buffer.push('')
const syntheticId = idSeparators.generateIdFromTitle(navtitleAsciiDoc, idSeparators)
let sectionTitle = navtitleAsciiDoc
if (urlType === 'external') {
sectionTitle = `${url}[${navtitleAsciiDoc.replace(/\]/g, '\\]')}]`
} else if (urlType === 'internal' && !unresolved) {
const resource = files.find((it) => it.pub.url === url)
if (resource) {
if (resource.src.family === 'page' && pagesInOutline.has(resource.pub.url)) {
const refid = generateScopedId(resource.src, componentVersion, idSeparators, filetype, true)
sectionTitle = `xref:${refid}[${navtitleAsciiDoc.replace(/\]/g, '\\]')}]`
} else if (siteRoot) {
sectionTitle = `${resolveLinkTarget(resource, siteRoot, pubRoot, linkReferenceStyle, true)}[${navtitleAsciiDoc.replace(/\]/g, '\\]')}]`
}
}
}
let hlevel = level + 1
if (hlevel > 6) {
hlevel = 6
buffer.push(`[discrete#${syntheticId}]`)
} else {
buffer.push(`[#${syntheticId}]`)
}
buffer.push(`${'='.repeat(hlevel)} ${sectionTitle}`)
}
}
if (hasItems) {
const nextLevel = level + 1
// NOTE: drop first child if same as parent; should we keep if content is different?
;(urlType === 'internal' && urlType === items[0].urlType && url === items[0].url && !items[0].items
? items.slice(1)
: items
).forEach((item) => {
mergeAsciiDoc(
loadAsciiDoc,
contentCatalog,
buffer,
componentVersion,
item,
files,
pagesInOutline,
idSeparators,
asciidocConfig,
mutableAttributes,
assemblyModel,
lastComponentVersion,
nextLevel,
atBookRoot
)
})
}
return buffer
}
function processDocumentHeader (doc, lines, ignoreLines, isRootPage) {
const entries = []
let assemblyHeaderAttributes = isRootPage && doc.getAttribute('assembly-header-attributes', '%authors')
assemblyHeaderAttributes = new Set(assemblyHeaderAttributes ? assemblyHeaderAttributes.split(/, */) : undefined)
if (assemblyHeaderAttributes.size) {
if (assemblyHeaderAttributes.has('%authors')) {
const authors = doc.getAuthors()
if (authors.length) {
entries.push({
type: 'author_line',
promote: true,
lines: [
authors
.map((author) => (author.getEmail() ? `${author.getName()} <${author.getEmail()}>` : author.getName()))
.join('; '),
],
})
}
assemblyHeaderAttributes.delete('%authors')
}
const headerAttributes = doc.source_header_attributes
for (const name of assemblyHeaderAttributes) headerAttributes.$delete(name)
}
const doctitleIdx = doc.getHeader().getLineNumber() - 1
const end = doc.getBlocks()[0]?.getLineNumber() ?? lines.length
let belowDoctitle, current, open
const implicitLines = []
for (let idx = 0; idx < end; idx++) {
if (idx === doctitleIdx) {
lines[idx] = undefined
ignoreLines.push(idx)
belowDoctitle = true
continue
}
const line = lines[idx]
if (open === ':' || open === '-:') {
if (open === ':') current.lines.push(line)
if (!line || !line.endsWith(' \\')) current = open = undefined
} else if (line) {
let attributeEntryMatch
const chr0 = line.charAt()
if (chr0 === '/' && line.charAt(1) === '/') {
if (line.startsWith('////') && line === '/'.repeat(line.length)) {
if (open) {
current.lines.push(line)
if (open === line) current = open = undefined
} else {
entries.push((current = { type: 'block_comment', lines: [(open = line)] }))
}
} else if (open) {
current.lines.push(line)
} else if (belowDoctitle && line.charAt(2) === '/') {
break
} else {
entries.push({ type: 'line_comment', lines: [line] })
current = undefined
}
} else if (open) {
current.lines.push(line)
} else if (chr0 === ':' && ~line.indexOf(':', 2) && (attributeEntryMatch = line.match(ATTR_ENTRY_RX))) {
let name = attributeEntryMatch[1]
const negated = name.charAt() === '!' || name.charAt(name.length - 1) === '!'
if (negated) name = name.replace('!', '')
if (DISCARD_ATTRIBUTE_NAMES.includes(name)) {
if (line.endsWith(' \\')) open = '-:' // disallow value continuation
} else {
if (line.endsWith(' \\')) open = ':'
entries.push((current = { type: 'attribute_entry', name, negated, lines: [line], lineno: idx + 1 }))
if (assemblyHeaderAttributes.has(name)) current.promote = true
}
} else if (belowDoctitle) {
if (implicitLines.length === 2 || !/[\p{Alpha}0-9]/u.test(chr0)) break
implicitLines.push(line)
} else if (chr0 === '[' && line.charAt(line.length - 1) === ']') {
current = undefined
const attrlist = line
.slice(1, -1)
.trim()
.replace(/(?:^\w[\w-]*(?!=|[\w-]))?([#.]\w[\w-]*)*/, '')
.replace(NAMED_ID_ATTR_RX, '')
if (attrlist) entries.push({ type: 'attrlist', lines: ['[' + attrlist + ']'] })
}
} else if (belowDoctitle) {
break
}
lines[idx] = undefined
ignoreLines.push(idx)
}
return entries
}
function generateSlug (string) {
if (string === 'index') return string
return string
.toLowerCase()
.replace(/<[^>]+>/g, '')
.replace(CHAR_REF_RX, (_, name, dec, hex) => {
if (name) return BUILT_IN_NAMED_ENTITIES[name] ?? '?'
return String.fromCharCode(dec ? parseInt(dec, 10) : parseInt(hex, 16))
})
.replace(/[\x27\u2019]/g, '')
.replace(/[^\p{Alpha}0-9-]/gu, '-')
.replace(/^-+|-+$|(-)-+/g, '$1')
}
function sanitizeSlug (slug) {
return slug.replace(/ /g, '-').replace(/[^\p{Alpha}0-9_.-]/gu, '-')
}
function fixSectionLevels (sections, expectedLevel) {
sections.forEach((sect) => {
const forceLevel = expectedLevel ?? Math.min(1, sect.getLevel())
if (sect.getLevel() !== forceLevel) sect.level = forceLevel
if (sect.hasSections()) fixSectionLevels(sect.getSections(), forceLevel + 1)
})
}
// NOTE: blank lines at top and bottom of document create mismatch when using line numbers to navigate source lines
// IMPORTANT: this must not leave behind lines the parser will drop!
// IDEA: another option is to capture initial lineno of reader and use as offset (but preseves those blank lines)
function trimAsciiDoc (buffer) {
return Buffer.from(
buffer
.toString()
.replace(/^(?:[ \t]*\r\n?|[ \t]*\n)+/, '')
.trimRight()
)
}
function safePush (onto, entries) {
try {
onto.push(...entries)
} catch (err) {
/* istanbul ignore if */
if (!(err instanceof RangeError)) throw err
for (const e of entries) onto.push(e)
}
}
function generateIdFromTitle (titleAsciiDoc, idSeparators) {
const Section = this.$class().$const_get('::Asciidoctor::Section')
const baseId = Section.$generate_id(titleAsciiDoc, this)
this.getCatalog().refs['$[]='](baseId, true)
return `_${idSeparators.coordinate}${baseId}`
}
module.exports = produceAssemblyFile