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.

860 lines (838 loc) 34.2 kB
'use strict' const createAsciiDocFile = require('./util/create-asciidoc-file') const parseResourceRef = require('./util/parse-resource-ref') const path = require('node:path/posix') const sanitize = require('./util/sanitize') const unconvertInlineAsciiDoc = require('./util/unconvert-inline-asciidoc') const AttributeEntryRx = /^:([^:-][^:]*):(?: .*)?$/ const BuiltInNamedEntities = { amp: '&', apos: "'", gt: '>', lt: '<', nbsp: ' ', quot: '"' } const CharRefRx = /&(?:([a-z][a-z]+\d{0,2})|#(?:(\d{2,6})|x([a-z\d]{2,5})));/g const DiscardAttributes = 'doctype leveloffset assembly-navtitle assembly-style underscore'.split(' ') const ReservedIdNames = 'content header footnotes footer footer-text premable toc toctitle'.split(' ') 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, componentVersion) const buffer = mergeAsciiDoc( loadAsciiDoc, contentCatalog, buildAsciiDocHeader(componentVersion, outline.content, assemblyModel), componentVersion, outline, files, pagesInOutline, asciidocConfig, mutableAttributes, assemblyModel ) const rootLevel = assemblyModel.rootLevel const stem = rootLevel === 0 ? 'index' : generateSlug(buffer.navtitle) const downloadStem = [componentVersion.name, componentVersion.version, rootLevel === 0 ? '' : 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: componentVersion.name, version: componentVersion.version, module: 'ROOT', family: 'export', relative: stem + '.adoc', }, }) } 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 buffer = [ `= ${doctitle}`, ...(version ? [`:revnumber: ${version}`] : []), ...(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: ${componentVersion.displayVersion}`, `:page-component-title: ${componentVersion.title}`, ] return Object.assign(buffer, { navtitle }) } function selectPagesInOutline (outlineEntry, pagesByUrl, componentVersion, 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, componentVersion, accum) return accum } function mergeAsciiDoc ( loadAsciiDoc, contentCatalog, buffer, componentVersion, outlineEntry, files, pagesInOutline, 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, embedReferenceStyle: embedRefStyle, linkReferenceStyle: linkRefStyle, outDirname, siteRoot, xmlIds, logger, } = assemblyModel // 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 atBookRoot = atDocumentRoot && !level && doctype === 'book' && (supportsParts = true) const hasItems = items.length > 0 const pubRoot = outDirname ? '/' + outDirname : '' const idSeparator = xmlIds ? '-' : ':' const idScopeSeparator = idSeparator.repeat(3) const idCoordinateSeparator = idSeparator === '-' ? '----' : idSeparator if (page && !pagesInOutline.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) let asciidoctorLogger = doc.getLogger() if (logger) { asciidoctorLogger = Object.assign(asciidoctorLogger.$dup(), { delegate: { warn () { return logger.warn.apply(logger, arguments) }, }, }) } 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}` } } } if (atDocumentRoot) { const authors = doc.getAuthors() if (authors.length) { const authorLine = authors .map((author) => { const email = author.getEmail() return email ? `${author.getName()} <${author.getEmail()}>` : author.getName() }) .join('; ') buffer.splice(1, 0, authorLine) } } // NOTE: in Antora, docname is relative src path from module without file extension const docname = doc.getAttribute('docname') const { idPrefix, id: idScope } = generateId(page.src, componentVersion, idCoordinateSeparator, idScopeSeparator) let pageFragment = '' let pageRoles = '' let pageStyle = doc.getAttribute('assembly-style', '') 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 = [] buffer.inBody = true 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()) pageRoles = processDocumentHeader(doc, lines, buffer, ignoreLines) 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 = `#${idPrefix}${doc.getId() ?? pageStyle}` buffer.unshift(`[#${idScope}]`) } else { pageFragment = `#${idScope}` } heading = { title: htitleAsciiDoc, level: part ? 1 : 2 } nextSectionLevel++ } } else if (level) { if (atDocumentRoot && navtitlePlain === componentVersion.title) { level-- } else { pageFragment = `#${idScope}` 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(`[#${idScope}]`) } 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('') // NOTE: try to toggle sectids; otherwise, fallback to globally unique synthetic ID let toggleSectids, syntheticId if (doc.isAttribute('sectids')) { if (doc.isAttributeLocked('sectids')) { syntheticId = `__object-id-${getObjectId(outlineEntry)}` } else { buffer.push(':!sectids:') toggleSectids = true } } let hlevel = level + 2 if (hlevel > 6) { const blockStyle = `discrete.h${hlevel}` hlevel = 6 buffer.push(syntheticId ? `[${blockStyle}#${syntheticId}]` : `[${blockStyle}]`) } else if (syntheticId) { buffer.push(`[#${syntheticId}]`) } buffer.push(`${'='.repeat(hlevel)} ${overviewTitle}`) if (toggleSectids) buffer.push(':sectids:') } pagesInOutline.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(AttributeEntryRx) || ['', ''])[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 `<<${idPrefix}${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, `[[${idPrefix}$1$2]]`) } if (~line.indexOf('xref:')) { // Q: should we allow : as first character of target? line = line.replace(/(?<![\\+])xref:((?:\.\/)?[\p{Alpha}0-9_/.{#].*?)\[(|.*?[^\\])\]/gu, (_, target, text) => { let fragment, resource, resourceRef const hashIdx = target.indexOf('#') if (~hashIdx) { resourceRef = target.slice(0, hashIdx) fragment = target.slice(hashIdx + 1) } else if (target.endsWith('.adoc') || ~target.indexOf('$')) { resourceRef = target fragment = '' } else { fragment = target } // Q: should we validate the internal ID here? if (!resourceRef) return `xref:${idPrefix}${fragment}[${text}]` const resourceId = parseResourceRef(resourceRef, page.src, 'page', contentCatalog) if (resourceId.family !== 'page' || !(resource = pagesInOutline.get(createResourceKey(resourceId)))) { if ((resource = contentCatalog.getById(resourceId))?.pub) { text ||= resource.asciidoc?.xreftext || target if (siteRoot || linkRefStyle === 'relative') { return `${resolveLinkTarget(resource, siteRoot, pubRoot, linkRefStyle)}${fragment && '#' + fragment}[${text}]` } asciidoctorLogger.warn( doc.createLogMessage( `Cannot create external ${resourceId.family} reference in assembly because site URL is unknown: ${target}`, { source_location: { file: relative, lineno: idx + 1 } } ) ) } const linkAttrlist = text ? (~text.indexOf(',') ? `"${text}"` : text) + ',role=unresolved' : 'role=unresolved' return `link:${target}[${linkAttrlist}]` } if (fragment === resource.asciidoc.id) fragment = '' if ( text && (assemblyModel.dropExplicitXrefText === 'always' || (assemblyModel.dropExplicitXrefText === 'if-redundant' && text === resource.title)) ) { text = '' } const refid = generateId( resource.src, componentVersion, idCoordinateSeparator, idScopeSeparator, text.length > 0, fragment ).id return `xref:${refid}[${text}]` }) } if (~line.indexOf('link:{attachmentsdir}/')) { line = line.replace(/(?<![\\+])link:\{attachmentsdir\}\/([^\s[]+)\[(|.*?[^\\])\]/g, (m, relative, text) => { const attachment = (siteRoot || linkRefStyle === 'relative') && contentCatalog.getById({ component: componentVersion.name, version: componentVersion.version, module: module_, family: 'attachment', relative, }) // TODO: handle unresolved attachment page return attachment?.out ? `${resolveLinkTarget(attachment, siteRoot, pubRoot, linkRefStyle)}[${text}]` : m }) } if (~line.indexOf('image:') && !line.startsWith('image::')) { line = line.replace(/(?<![\\+])image:([^:\s[](?:[^[]*[^\s[])?)\[([^\]]*)\]/g, (m, target, attrlist) => { let image if ( isResourceSpec(target) && (image = contentCatalog.resolveResource(target, page.src, 'image', ['image']))?.out ) { if (filetype !== 'html') { pagesInOutline.assembled.assets.add(image) return `image:${resolveEmbedTarget(image, outDirname, embedRefStyle, true)}[${attrlist}]` } if (siteRoot || linkRefStyle === 'relative') { pagesInOutline.assembled.assets.add(image) return `image:${resolveLinkTarget(image, siteRoot, pubRoot, linkRefStyle)}[${attrlist}]` } asciidoctorLogger.warn( doc.createLogMessage( `Cannot create external image reference in assembly because site URL is unknown: ${target}`, { source_location: { file: relative, lineno: idx + 1 } } ) ) } return m }) } 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, idPrefix, 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 >= 0) { const target = block.getAttribute('target') if (isResourceSpec(target)) { const image = contentCatalog.resolveResource(target, page.src, 'image', ['image']) // FIXME: handle (or report) case when image is not resolved if (image?.out && (filetype !== 'html' || siteRoot)) { const attrlist = line.slice(line.indexOf('[') + 1, -1) pagesInOutline.assembled.assets.add(image) lines[idx] = filetype === 'html' ? `${prefix}image::${resolveLinkTarget(image, siteRoot, pubRoot, linkRefStyle, false)}[${attrlist}]` : `${prefix}image::${resolveEmbedTarget(image, outDirname, embedRefStyle)}[${attrlist}]` } } 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, idPrefix) } }) 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) || DiscardAttributes.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('') // NOTE: try to toggle sectids; otherwise, fallback to globally unique synthetic ID // Q: should we unset docname, page-module, etc? let toggleSectids, syntheticId if (!('sectids' in asciidocConfig.attributes)) { buffer.push(':!sectids:') toggleSectids = true } else if (typeof asciidocConfig.attributes.sectids === 'string') { if ('sectids' in mutableAttributes) { buffer.push(':!sectids:') toggleSectids = true } else { syntheticId = `__object-id-${global.Opal.hash(outlineEntry).$object_id()}` } } 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 = generateId(resource.src, componentVersion, idCoordinateSeparator, idScopeSeparator, true).id sectionTitle = `xref:${refid}[${navtitleAsciiDoc.replace(/\]/g, '\\]')}]` } else if (siteRoot) { sectionTitle = `${resolveLinkTarget(resource, siteRoot, pubRoot, linkRefStyle)}[${navtitleAsciiDoc.replace(/\]/g, '\\]')}]` } } } let hlevel = level + 1 if (hlevel > 6) { hlevel = 6 buffer.push(syntheticId ? `[discrete#${syntheticId}]` : '[discrete]') } else if (syntheticId) { buffer.push(`[#${syntheticId}]`) } buffer.push(`${'='.repeat(hlevel)} ${sectionTitle}`) if (toggleSectids) buffer.push(':sectids:') } } 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, asciidocConfig, mutableAttributes, assemblyModel, lastComponentVersion, nextLevel, atBookRoot ) }) } return buffer } function processDocumentHeader (doc, lines, buffer, ignoreLines) { const doctitleIdx = doc.getHeader().getLineNumber() - 1 const end = doc.getBlocks()[0]?.getLineNumber() ?? lines.length let belowDoctitle let 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 === ':') buffer.push(line) if (!line || !line.endsWith(' \\')) open = undefined } else if (line) { const chr0 = line.charAt() let attributeEntryMatch if (chr0 === '/' && line.charAt(1) === '/') { if (line.startsWith('////')) { open = open ? (open === line ? undefined : open) : line } else if (belowDoctitle && !open && line.charAt(2) === '/') { break } buffer.push(line) } else if (open) { buffer.push(line) } else if (chr0 === ':' && ~line.indexOf(':', 2) && (attributeEntryMatch = line.match(AttributeEntryRx))) { const attributeName = attributeEntryMatch[1].replace('!', '') if (DiscardAttributes.includes(attributeName)) { if (line.endsWith(' \\')) open = '-:' // disallow value continuation } else { if (line.endsWith(' \\')) open = ':' buffer.push(line) } } 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) === ']') { const attrlist = line .slice(1, -1) .trim() .replace(/(?:^\w[\w-]*(?!=|[\w-]))?([#.%]\w[\w-]*)*/, '') if (attrlist) buffer.push('[' + attrlist + ']') } } else if (belowDoctitle) { break } lines[idx] = undefined ignoreLines.push(idx) } return doc .getRoles() .map((role) => '.' + role) .join('') } function generateSlug (title) { return title .toLowerCase() .replace(/<[^>]+>/g, '') .replace(CharRefRx, (_, name, dec, hex) => { if (name) return BuiltInNamedEntities[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 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) }) } function rewriteStyleAttribute (block, lines, idx, idPrefix, replacementStyle = '') { let prevLine = lines[idx - 1] const char0 = prevLine?.charAt() if (char0) { if ( (char0 === '.' && /^\.\.?[^ \t.]/.test(prevLine)) || (char0 === '[' && prevLine.charAt(1) === '[' && /^\[\[(?:|[\p{Alpha}_:][\p{Alpha}0-9_\-:.]*(?:, *.+)?)\]\]$/u.test(prevLine)) ) { return rewriteStyleAttribute(block, lines, idx - 1, idPrefix, replacementStyle) } } let cellSpec if ( char0 && (char0 === '[' || (block.getDocument().isNested() && (cellSpec = prevLine.match(/^([^[|]*)\| *(\[.+)/)))) && prevLine.charAt(prevLine.length - 1) === ']' ) { if (cellSpec) { prevLine = cellSpec[2] cellSpec = cellSpec[1] } let rawStyle const commaIdx = prevLine.indexOf(',') if (~commaIdx) { rawStyle = prevLine.slice(1, commaIdx) if (~rawStyle.indexOf('=')) rawStyle = undefined } else if (!~prevLine.indexOf('=')) { rawStyle = prevLine.slice(1, prevLine.length - 1) } if (rawStyle) { if (~rawStyle.indexOf('#')) { prevLine = prevLine.replace(/#[^.%,\]]+/, `#${idPrefix}${block.getId()}`) if (replacementStyle) prevLine = prevLine.replace(/^[^#.%,\]]+/, `[${replacementStyle}`) } else { prevLine = `[${ replacementStyle ? rawStyle.replace(/^[^.%]*/, replacementStyle) : rawStyle }#${idPrefix}${block.getId()}${prevLine.slice(rawStyle.length + 1)}` } } else { prevLine = `[${replacementStyle}#${idPrefix}${block.getId()}${rawStyle == null ? ',' : ''}${prevLine.slice(1)}` } if (cellSpec) prevLine = `${cellSpec}|${prevLine}` lines[idx - 1] = prevLine } else { lines.splice(idx, 0, `[${replacementStyle}#${idPrefix}${block.getId()}]`) } } function isResourceSpec (str) { return !(~str.indexOf(':') && (~str.indexOf('://') || (str.startsWith('data:') && ~str.indexOf(',')))) } function getObjectId (obj) { return global.Opal.id(obj) } // 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 resolveEmbedTarget (resource, outDirname, referenceStyle, escapeForInline) { const target = referenceStyle === 'output-relative' ? resource.out.path : path.relative(outDirname + '/', resource.out.path) return escapeForInline ? target.replace(/_/g, '{underscore}') : target } function resolveLinkTarget (resource, siteRoot, pubRoot, referenceStyle, escapeForInline = true) { let target if (resource.site?.url) { target = ['', resource.pub.url] } else { switch (referenceStyle) { case 'absolute': target = ['', siteRoot.url + resource.pub.url] break case 'root-relative': target = ['link:', siteRoot.path + resource.pub.url] break default: target = ['link:', computeRelativeUrl(pubRoot + '/', resource.pub.url)] } } if (escapeForInline) target[1] = target[1].replace(/_/g, '{underscore}') return target.join('') } function computeRelativeUrl (from, to) { const rel = path.relative(from, to) return to.charAt(to.length - 1) === '/' ? rel + '/' : rel } function createResourceKey ({ component, version, module: mod, family, relative }) { return `${version}@${component}:${mod === 'ROOT' ? '' : mod}:${family === 'page' ? '' : family + '$'}${relative}` } function generateId (componentSrc, componentVersion, coordinateSep, scopeSep, asXrefTarget, fragment) { let { component, module: mod, relative } = componentSrc let id = relative.replace(/\.adoc$/, '').replace(/[/.]/g, '-') if (component !== componentVersion.name) { id = [component, mod === 'ROOT' ? '' : mod, id].join(coordinateSep) } else if (mod !== 'ROOT') { if (asXrefTarget && coordinateSep === ':' && /(?:pass|stem)$/.test(mod) && /^[a-z]+(?:,[a-z-]+)*$/.test(id)) { mod = mod.replace(/(?:pass|stem)$/, '\\$&') } id = mod + coordinateSep + id } else if (ReservedIdNames.includes(id)) { id += scopeSep scopeSep = '' } const idPrefix = id + scopeSep if (fragment) id = idPrefix + fragment return { idPrefix, id } } module.exports = produceAssemblyFile