UNPKG

@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
'use strict' 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