@antora/assembler
Advanced tools
Comparing version 1.0.0-alpha.7 to 1.0.0-alpha.8
@@ -6,2 +6,3 @@ 'use strict' | ||
const PromiseQueue = require('./util/promise-queue') | ||
const runCommand = require('./util/run-command') | ||
@@ -27,3 +28,3 @@ async function assembleContent (playbook, contentCatalog, converter, { siteCatalog, configSource }) { | ||
return new PromiseQueue({ concurrency: buildConfig.processLimit }) | ||
.add(aggregateDocuments.map((doc) => () => converter.call(this, doc, buildConfig))) | ||
.add(aggregateDocuments.map((doc) => () => converter.call(this, doc, buildConfig, runCommand))) | ||
.toPromise() | ||
@@ -30,0 +31,0 @@ .then((files) => { |
@@ -24,7 +24,7 @@ 'use strict' | ||
const negated = pattern.charAt() === '!' | ||
if (negated) pattern = pattern.substr(1) | ||
if (negated) pattern = pattern.slice(1) | ||
let version | ||
const separatorIdx = pattern.search(VERSION_SEPARATOR_RX) | ||
if (~separatorIdx) { | ||
pattern = (version = true) && `${pattern.substr(0, separatorIdx)}%${pattern.substr(separatorIdx + 1) || '*'}` | ||
pattern = (version = true) && `${pattern.slice(0, separatorIdx)}%${pattern.slice(separatorIdx + 1) || '*'}` | ||
} | ||
@@ -31,0 +31,0 @@ return Object.assign(makePicomatchRx(pattern, PICOMATCH_OPTS), { |
@@ -32,2 +32,3 @@ 'use strict' | ||
asciidocAttrs['page-partial'] = null | ||
asciidocAttrs['loader-assembler'] = '' | ||
if (config.componentVersions == null) { | ||
@@ -50,3 +51,3 @@ config.componentVersions = ['*'] | ||
} | ||
build.cwd = playbook.dir // use playbook.dir the purpose of finding and loading require scripts | ||
build.cwd = playbook.dir // use playbook.dir for the purpose of finding and loading require scripts | ||
if (!('clean' in build) && 'output' in playbook) build.clean = playbook.output.clean | ||
@@ -67,3 +68,3 @@ if (!('publish' in build)) build.publish = true | ||
for (const [k, v] of Object.entries(o)) { | ||
const camelKey = k.charAt() + k.substr(1).replace(/_([a-z])/g, (_, l) => l.toUpperCase()) | ||
const camelKey = k.charAt() + k.slice(1).replace(/_([a-z])/g, (_, l) => l.toUpperCase()) | ||
accum[camelKey] = ~stopPaths.indexOf(pathPrefix + camelKey) ? v : camelCaseKeys(v, stopPaths, pathPrefix + camelKey) | ||
@@ -70,0 +71,0 @@ } |
@@ -5,2 +5,3 @@ 'use strict' | ||
const path = require('node:path/posix') | ||
const sanitize = require('./util/sanitize') | ||
@@ -18,8 +19,15 @@ function produceAggregateDocument ( | ||
) { | ||
const pagesInOutline = selectPagesInOutline( | ||
outline, | ||
pages.filter((it) => it.out) | ||
) | ||
const pagesInOutline = selectPagesInOutline(outline, pages) | ||
const navtitle = outline.content | ||
const stem = generateStem(componentVersion, navtitle) | ||
const templateFile = contentCatalog.addFile({ | ||
src: { | ||
component: componentVersion.name, | ||
version: componentVersion.version, | ||
module: 'ROOT', | ||
family: 'page', | ||
relative: generateSlug(navtitle), | ||
}, | ||
}) | ||
const path_ = `${templateFile.out.path}.adoc` | ||
contentCatalog.removeFile(templateFile) | ||
const header = buildAsciiDocHeader(componentVersion, navtitle, doctype) | ||
@@ -37,13 +45,13 @@ const body = aggregateAsciiDoc( | ||
) | ||
const relativeSrcPath = `${stem}.adoc` | ||
return new File({ | ||
aggregate: true, | ||
asciidoc: asciidocConfig, | ||
contents: Buffer.from([...header, ...body].join('\n') + '\n'), | ||
mediaType: 'text/asciidoc', | ||
path: relativeSrcPath, | ||
path: path_, | ||
src: { | ||
component: componentVersion.name, | ||
version: componentVersion.version, | ||
basename: path.basename(relativeSrcPath), | ||
stem, | ||
basename: path.basename(path_), | ||
stem: path.basename(path_, '.adoc'), | ||
extname: '.adoc', | ||
@@ -55,3 +63,5 @@ }, | ||
function buildAsciiDocHeader (componentVersion, navtitle, doctype = 'book') { | ||
const doctitle = navtitle === componentVersion.title ? navtitle : `${componentVersion.title}: ${navtitle}` | ||
const [navtitlePlain, navtitleAsciiDoc] = sanitize(navtitle) | ||
let doctitle = navtitleAsciiDoc | ||
if (navtitlePlain !== componentVersion.title) doctitle = `${componentVersion.title}: ${doctitle}` | ||
const version = componentVersion.version === 'master' ? '' : componentVersion.version | ||
@@ -95,2 +105,3 @@ return [ | ||
sectionMergeStrategy, | ||
lastComponentVersion = componentVersion, | ||
level = 0 | ||
@@ -102,6 +113,13 @@ ) { | ||
const { content: navtitle, items, unresolved, urlType, url } = outlineEntry | ||
const [navtitlePlain, navtitleAsciiDoc, navtitlePass] = sanitize(navtitle) | ||
const siteUrl = ((val) => { | ||
if (!val || val === '/') return '' | ||
return val.charAt(val.length - 1) === '/' ? val.slice(0, val.length - 1) : val | ||
})(asciidocConfig.attributes['site-url']) | ||
// FIXME: ideally, resource ID would be stored in navigation so we can look up the page more efficiently | ||
let page = urlType === 'internal' && !unresolved ? pagesInOutline.get(url) : undefined | ||
if (page && pagesInOutline.aggregated?.includes(page)) page = undefined | ||
if (page) { | ||
let contents = page.src.contents | ||
if (contents == null) return buffer | ||
// NOTE: blank lines at top and bottom of document create mismatch when using line numbers to navigate source lines | ||
@@ -116,4 +134,5 @@ // IMPORTANT: this must not leave behind lines the parser will drop! | ||
) | ||
;(pagesInOutline.aggregated ??= []).push(page) | ||
page = new page.constructor(Object.assign({}, page, { contents, mediaType: 'text/asciidoc' })) | ||
const { module: module_, relative, origin } = page.src | ||
const { component, version, module: module_, relative, origin } = page.src | ||
const doc = loadAsciiDoc(page, contentCatalog, asciidocConfig) | ||
@@ -124,5 +143,24 @@ const refs = doc.getCatalog().refs | ||
const docnameForId = docname.replace(/[/]/g, '::').replace(/[.]/g, '-') | ||
const idprefix = `${module_ === 'ROOT' ? '' : module_ + ':'}${docnameForId}:::` | ||
const scopeId = component !== componentVersion.name | ||
const idprefix = | ||
(scopeId ? component + ':' : '') + | ||
(module_ === 'ROOT' ? (scopeId ? ':' : '') : module_ + ':') + | ||
docnameForId + | ||
':::' | ||
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_}`) | ||
@@ -139,3 +177,3 @@ buffer.push(`:page-relative-src-path: ${relative}`) | ||
if (level) { | ||
if (level === 1 && navtitle === componentVersion.title) { | ||
if (level === 1 && navtitlePlain === componentVersion.title) { | ||
level-- | ||
@@ -150,3 +188,3 @@ } else { | ||
} | ||
buffer.push(`${'='.repeat(hlevel)} ${navtitle}`) | ||
buffer.push(`${'='.repeat(hlevel)} ${navtitleAsciiDoc}`) | ||
} | ||
@@ -183,6 +221,2 @@ } else { | ||
} | ||
const siteUrl = ((val) => { | ||
if (!val || val === '/') return '' | ||
return val.charAt(val.length - 1) === '/' ? val.substr(0, val.length - 1) : val | ||
})(doc.getAttribute('site-url')) | ||
const lines = doc.getSourceLines() | ||
@@ -264,4 +298,4 @@ const ignoreLines = [] | ||
if (~hashIdx) { | ||
pagePart = target.substr(0, hashIdx) | ||
fragment = target.substr(hashIdx + 1) | ||
pagePart = target.slice(0, hashIdx) | ||
fragment = target.slice(hashIdx + 1) | ||
// TODO: for now, assume .adoc; in the future, consider other file extensions | ||
@@ -282,16 +316,20 @@ if (pagePart && !pagePart.endsWith('.adoc')) pagePart += '.adoc' | ||
if (~pagePart.indexOf('@') || /:.*:/.test(pagePart)) { | ||
if (siteUrl && (targetPage = contentCatalog.resolvePage(pagePart, page.src)) && targetPage.out) { | ||
text ||= targetPage.asciidoc?.xreftext || target | ||
return `${siteUrl}${targetPage.pub.url}${fragment && '#' + fragment}[${text}]` | ||
} | ||
// TODO: handle unresolved page better | ||
return siteUrl && (targetPage = contentCatalog.resolvePage(pagePart, page.src)) && targetPage.out | ||
? `${siteUrl}${targetPage.pub.url}${fragment && '#' + fragment}[${text}]` | ||
: m | ||
return m | ||
} else if (pagePart.indexOf(':') < 0) { | ||
if (module_ !== 'ROOT') pagePart = `${module_}:${pagePart}` | ||
} else if (pagePart.startsWith('ROOT:')) { | ||
pagePart = pagePart.substr(5) | ||
pagePart = pagePart.slice(5) | ||
} | ||
if (!(targetPage = pagesInOutline.get(pagePart))) { | ||
if (siteUrl && (targetPage = contentCatalog.resolvePage(pagePart, page.src)) && targetPage.out) { | ||
text ||= targetPage.asciidoc?.xreftext || target | ||
return `${siteUrl}${targetPage.pub.url}${fragment && '#' + fragment}[${text}]` | ||
} | ||
// TODO: handle unresolved page better | ||
return siteUrl && (targetPage = contentCatalog.resolvePage(target, page.src)) && targetPage.out | ||
? `${siteUrl}${targetPage.pub.url}${fragment && '#' + fragment}[${text}]` | ||
: m | ||
return m | ||
} | ||
@@ -317,2 +355,3 @@ pagePart = pagePart | ||
}) | ||
// TODO: handle unresolved attachment page | ||
return attachment && attachment.out | ||
@@ -370,3 +409,3 @@ ? `${siteUrl}${attachment.pub.url.replace(/_/g, '{underscore}')}[${text}]` | ||
let imageMacroOffset = ( | ||
lastImageMacroAt?.[0] === idx ? line.substr(0, lastImageMacroAt[1]) : line | ||
lastImageMacroAt?.[0] === idx ? line.slice(0, lastImageMacroAt[1]) : line | ||
).lastIndexOf('image::') | ||
@@ -376,5 +415,5 @@ if (imageMacroOffset > 0) { | ||
block.getDocument().isNested() && | ||
(prefix = line.substr(0, imageMacroOffset)).trimRight().endsWith('|') | ||
(prefix = line.slice(0, imageMacroOffset)).trimRight().endsWith('|') | ||
) { | ||
line = line.substr(prefix.length) | ||
line = line.slice(prefix.length) | ||
} else { | ||
@@ -390,3 +429,3 @@ imageMacroOffset = -1 | ||
if (image && image.out) { | ||
const boxedAttrlist = line.substr(line.indexOf('[')) | ||
const boxedAttrlist = line.slice(line.indexOf('[')) | ||
image.out.assembled = true | ||
@@ -418,3 +457,11 @@ lines[idx] = `${prefix}image::${image.out.path}${boxedAttrlist}` | ||
} | ||
} else if (val != null && !(doc.isAttributeLocked(name) || name === 'doctype' || name === 'leveloffset')) { | ||
} else if ( | ||
!( | ||
val == null || | ||
doc.isAttributeLocked(name) || | ||
name === 'doctype' || | ||
name === 'leveloffset' || | ||
name === 'underscore' | ||
) | ||
) { | ||
accum.push(`:!${name}:`) | ||
@@ -428,33 +475,40 @@ } | ||
} | ||
} else { | ||
if (level) { | ||
if (level === 1 && navtitle === componentVersion.title) { | ||
level-- | ||
} else { | ||
buffer.push('') | ||
// NOTE: try to toggle sectids; otherwise, fallback to globally unique synthetic ID | ||
let toggleSectids, syntheticId | ||
if (!('sectids' in asciidocConfig.attributes)) { | ||
} else if (level) { | ||
if (level === 1 && navtitlePlain === componentVersion.title) { | ||
level-- | ||
} else { | ||
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 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()}` | ||
} | ||
} else { | ||
syntheticId = `__object-id-${global.Opal.hash(outlineEntry).$object_id()}` | ||
} | ||
// Q: should we unset docname, page-module, etc? | ||
const sectionTitle = urlType === 'external' ? `${url}[${navtitle.replace(/\]/, '\\]')}]` : navtitle | ||
let hlevel = level + 1 | ||
if (hlevel > 6) { | ||
hlevel = 6 | ||
buffer.push(syntheticId ? `[discrete#${syntheticId}]` : '[discrete]') | ||
} else if (syntheticId) { | ||
buffer.push(`[#${syntheticId}]`) | ||
} | ||
let sectionTitle = navtitleAsciiDoc | ||
if (urlType === 'external') { | ||
sectionTitle = `${url}[${navtitlePass ? navtitleAsciiDoc : navtitleAsciiDoc.replace(/\]/g, '\\]')}]` | ||
} else if (urlType === 'internal' && !unresolved && siteUrl) { | ||
const resource = contentCatalog.getFiles().find((it) => it.pub.url === url) | ||
if (resource?.out) { | ||
sectionTitle = navtitlePass ? navtitleAsciiDoc : navtitleAsciiDoc.replace(/\]/g, '\\]') | ||
sectionTitle = `${siteUrl}${resource.pub.url}[${sectionTitle}]` | ||
} | ||
buffer.push(`${'='.repeat(hlevel)} ${sectionTitle}`) | ||
if (toggleSectids) buffer.push(':sectids:') | ||
} | ||
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:') | ||
} | ||
@@ -465,3 +519,7 @@ } | ||
if (items) { | ||
items.forEach((item) => { | ||
// 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) => { | ||
buffer.push( | ||
@@ -478,2 +536,3 @@ ...aggregateAsciiDoc( | ||
sectionMergeStrategy, | ||
lastComponentVersion, | ||
nextLevel | ||
@@ -487,15 +546,8 @@ ) | ||
function generateStem (componentVersion, title) { | ||
const { name, version } = componentVersion | ||
const segments = [] | ||
if (name !== 'ROOT') segments.push(name) | ||
if (version && version !== 'master') segments.push(version) | ||
segments.push( | ||
title | ||
.toLowerCase() | ||
.replace(/&.+?;|[^ \p{Alpha}0-9_\-.]/gu, '') | ||
.replace(/[ _.]/g, '-') | ||
.replace(/--+/g, '-') | ||
) | ||
return path.join(...segments) | ||
function generateSlug (title) { | ||
return title | ||
.toLowerCase() | ||
.replace(/&.+?;|[^ \p{Alpha}0-9_\-.]/gu, '') | ||
.replace(/[ _.]/g, '-') | ||
.replace(/--+/g, '-') | ||
} | ||
@@ -537,6 +589,6 @@ | ||
if (~commaIdx) { | ||
rawStyle = prevLine.substr(1, commaIdx - 1) | ||
rawStyle = prevLine.slice(1, commaIdx - 1) | ||
if (~rawStyle.indexOf('=')) rawStyle = undefined | ||
} else if (!~prevLine.indexOf('=')) { | ||
rawStyle = prevLine.substr(1, prevLine.length - 2) | ||
rawStyle = prevLine.slice(1, prevLine.length - 2) | ||
} | ||
@@ -550,6 +602,6 @@ if (rawStyle) { | ||
replacementStyle ? rawStyle.replace(/^[^.%]*/, replacementStyle) : rawStyle | ||
}#${idprefix}${block.getId()}${prevLine.substr(rawStyle.length + 1)}` | ||
}#${idprefix}${block.getId()}${prevLine.slice(rawStyle.length + 1)}` | ||
} | ||
} else { | ||
prevLine = `[${replacementStyle}#${idprefix}${block.getId()}${rawStyle == null ? ',' : ''}${prevLine.substr(1)}` | ||
prevLine = `[${replacementStyle}#${idprefix}${block.getId()}${rawStyle == null ? ',' : ''}${prevLine.slice(1)}` | ||
} | ||
@@ -556,0 +608,0 @@ if (cellSpec) prevLine = `${cellSpec}|${prevLine}` |
@@ -8,2 +8,4 @@ 'use strict' | ||
const IMAGE_MACRO_RX = /^image::?(.+?)\[(.*?)\]$/ | ||
function produceAggregateDocuments (loadAsciiDoc, contentCatalog, assemblerConfig) { | ||
@@ -24,16 +26,21 @@ const { insertStartPage, rootLevel, sectionMergeStrategy, asciidoc: assemblerAsciiDocConfig } = assemblerConfig | ||
}) | ||
const mergedAsciiDocAttributes = mergedAsciiDocConfig.attributes | ||
Object.entries(mergedAsciiDocAttributes).forEach(([name, val]) => { | ||
const match = name.endsWith('-image') && val.startsWith('image:') && IMAGE_MACRO_RX.exec(val) | ||
if (!(match && isResourceRef(match[1]))) return | ||
// Q should we allow image to be resolved relative to component version? | ||
const image = contentCatalog.resolveResource(match[1], undefined, 'image', ['image']) | ||
if (!image?.out) return | ||
mergedAsciiDocAttributes[name] = `image:${image.out.path}[${match[2]}]` | ||
image.out.assembled = true | ||
}) | ||
const rootEntry = { content: title } | ||
const startPage = contentCatalog.getComponentVersionStartPage(componentName, version) | ||
let startPageUrl | ||
if (startPage && insertStartPage) { | ||
if (includedInNav(navigation, (startPageUrl = startPage.pub.url))) { | ||
startPageUrl = undefined | ||
} else { | ||
Object.assign(rootEntry, { url: startPageUrl, urlType: 'internal' }) | ||
let startPage = contentCatalog.getComponentVersionStartPage(componentName, version) | ||
if (startPage && startPage.src.component === componentName && startPage.src.version === version) { | ||
if (insertStartPage && !includedInNav(navigation, startPage.pub.url)) { | ||
Object.assign(rootEntry, { url: startPage.pub.url, urlType: 'internal' }) | ||
} | ||
} | ||
// Q: should we always use a reference page here? | ||
const startPage_ = | ||
startPage ?? | ||
createFile({ | ||
} else { | ||
// Q: should we always use a reference page as startPage for computing mutableAttributes? | ||
startPage = createFile({ | ||
component: componentVersion.name, | ||
@@ -44,3 +51,4 @@ version: componentVersion.version, | ||
}) | ||
const mutableAttributes = selectMutableAttributes(loadAsciiDoc, contentCatalog, startPage_, mergedAsciiDocConfig) | ||
} | ||
const mutableAttributes = selectMutableAttributes(loadAsciiDoc, contentCatalog, startPage, mergedAsciiDocConfig) | ||
delete mutableAttributes.doctype | ||
@@ -62,6 +70,6 @@ accum = accum.concat( | ||
) | ||
mergedAsciiDocConfig.attributes.doctype = doctype | ||
mergedAsciiDocAttributes.doctype = doctype | ||
sourceHighlighter | ||
? (mergedAsciiDocConfig.attributes['source-highlighter'] = sourceHighlighter) | ||
: delete mergedAsciiDocConfig.attributes['source-highlighter'] | ||
? (mergedAsciiDocAttributes['source-highlighter'] = sourceHighlighter) | ||
: delete mergedAsciiDocAttributes['source-highlighter'] | ||
return accum | ||
@@ -120,2 +128,6 @@ }, | ||
function isResourceRef (target) { | ||
return ~target.indexOf(':') && !(~target.indexOf('://') || (target.startsWith('data:') && ~target.indexOf(','))) | ||
} | ||
// when root level is 0, merge the navigation into the rootEntry | ||
@@ -122,0 +134,0 @@ // when root level is 1, create navigation per navigation menu |
{ | ||
"name": "@antora/assembler", | ||
"version": "1.0.0-alpha.7", | ||
"version": "1.0.0-alpha.8", | ||
"description": "An extension library for Antora that assembles content from multiple pages into a single AsciiDoc file to converted and publish.", | ||
@@ -18,4 +18,4 @@ "license": "MPL-2.0", | ||
"test": "_mocha test", | ||
"prepublishOnly": "node $npm_config_local_prefix/npm/prepublishOnly.js", | ||
"postpublish": "node $npm_config_local_prefix/npm/postpublish.js" | ||
"prepublishOnly": "npx -y downdoc --prepublish", | ||
"postpublish": "npx -y downdoc --postpublish" | ||
}, | ||
@@ -40,4 +40,4 @@ "main": "lib/index.js", | ||
"devDependencies": { | ||
"@antora/asciidoc-loader": "3.0.1", | ||
"@antora/site-publisher": "3.0.1" | ||
"@antora/asciidoc-loader": "~3.1", | ||
"@antora/site-publisher": "~3.1" | ||
}, | ||
@@ -44,0 +44,0 @@ "engines": { |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
54740
14
1270