diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 8499dd0d39be16fff78608a5bb2325e74910d1fd..457338bf11612a446d035f9935e171fc39a06507 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -6,6 +6,10 @@ For a detailed view of what's changed, refer to the {url-repo}/commits[commit hi == Unreleased +=== Changed + +* convert page reference to internal reference for any page included in assembly (#127) + === Fixed * preserve empty line in multiline attribute entry that follows line with value continuation (#130) diff --git a/packages/assembler/lib/produce-assembly-file.js b/packages/assembler/lib/produce-assembly-file.js index ce4525012161d47e0dd5e268cae569f48bfb21c9..650afec3f5ead18498bb34a616198ee6dce94313 100644 --- a/packages/assembler/lib/produce-assembly-file.js +++ b/packages/assembler/lib/produce-assembly-file.js @@ -1,6 +1,7 @@ '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') @@ -81,10 +82,8 @@ 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) { - if (page.src.component === componentVersion.name && page.src.version === componentVersion.version) { - accum.set(`${page.src.module === 'ROOT' ? '' : page.src.module + ':'}${page.src.relative}`, page) - } - accum.set(page.pub.url, 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 @@ -139,7 +138,6 @@ function mergeAsciiDoc ( return buffer } const { component, version, module: module_, relative, origin, mediaType } = page.src - const topicPrefix = ~relative.indexOf('/') ? path.dirname(relative) + '/' : '' const pageAsAsciiDoc = new page.constructor( Object.assign({}, page, { contents: trimAsciiDoc(contents), mediaType }) ) @@ -171,18 +169,7 @@ function mergeAsciiDoc ( } // NOTE: in Antora, docname is relative src path from module without file extension const docname = doc.getAttribute('docname') - const docnameForId = docname.replace(/[/.]/g, '-') - const qualifyId = component !== componentVersion.name - let idScope = docnameForId - let idPrefix - if (qualifyId) { - idScope = [component, module_ === 'ROOT' ? '' : module_, idScope].join(idCoordinateSeparator) - } else if (module_ !== 'ROOT') { - idScope = module_ + idCoordinateSeparator + idScope - } else if (ReservedIdNames.includes(docnameForId)) { - idScope = idPrefix = idScope + idScopeSeparator - } - idPrefix ??= idScope + idScopeSeparator + const { idPrefix, id: idScope } = generateId(page.src, componentVersion, idCoordinateSeparator, idScopeSeparator) let pageFragment = '' let pageRoles = '' let pageStyle = doc.getAttribute('assembly-style', '') @@ -388,62 +375,38 @@ function mergeAsciiDoc ( if (~line.indexOf('xref:')) { // Q: should we allow : as first character of target? line = line.replace(/(? { - let relativePart, fragment, resource, isPage + let fragment, resource, resourceRef const hashIdx = target.indexOf('#') - const dollarIdx = target.indexOf('$') if (~hashIdx) { - relativePart = target.slice(0, hashIdx) + resourceRef = target.slice(0, hashIdx) fragment = target.slice(hashIdx + 1) - } else if (~dollarIdx || target.endsWith('.adoc')) { - relativePart = target + } else if (target.endsWith('.adoc') || ~target.indexOf('$')) { + resourceRef = target fragment = '' } else { fragment = target } // Q: should we validate the internal ID here? - if (!relativePart) return `xref:${idPrefix}${fragment}[${text}]` - if (~dollarIdx) { - if (relativePart.slice(dollarIdx).startsWith('$./')) { - relativePart = relativePart.slice(0, dollarIdx + 1) + topicPrefix + relativePart.slice(dollarIdx + 3) - } - if ((isPage = /\bpage\$/.test(relativePart))) relativePart = relativePart.replace('page$', '') - } else { - isPage = true - if (relativePart.startsWith('./')) relativePart = topicPrefix + relativePart.slice(2) - if (~hashIdx && !relativePart.endsWith('.adoc')) relativePart += '.adoc' - } - if (!isPage || ~relativePart.indexOf('@') || /:.*:/.test(relativePart)) { - if (siteRoot && (resource = contentCatalog.resolveResource(relativePart, page.src, 'page'))?.pub) { + if (!resourceRef) return `xref:${idPrefix}${fragment}[${text}]` + const resourceId = parseResourceRef(resourceRef, page.src, 'page', contentCatalog) + if (resourceId.family !== 'page') { + if (siteRoot && (resource = contentCatalog.getById(resourceId))?.pub) { text ||= resource.asciidoc?.xreftext || target return `${resolveLinkTarget(resource, siteRoot, pubRoot, linkRefStyle)}${fragment && '#' + fragment}[${text}]` } // TODO: handle unresolved resource better return m } - let targetModule - const colonIdx = relativePart.indexOf(':') - if (~colonIdx) { - targetModule = relativePart.slice(0, colonIdx) - relativePart = relativePart.slice(colonIdx + 1) - if (relativePart.startsWith('./')) relativePart = topicPrefix + relativePart.slice(2) - } else { - targetModule = module_ - } - const pageResourceRef = targetModule === 'ROOT' ? relativePart : `${targetModule}:${relativePart}` - if (!(resource = pagesInOutline.get(pageResourceRef))) { - if (siteRoot && (resource = contentCatalog.resolvePage(pageResourceRef, page.src))?.out) { + if (!(resource = pagesInOutline.get(createResourceKey(resourceId)))) { + if (siteRoot && (resource = contentCatalog.getById(resourceId))?.pub) { text ||= resource.asciidoc?.xreftext || target return `${resolveLinkTarget(resource, siteRoot, pubRoot, linkRefStyle)}${fragment && '#' + fragment}[${text}]` } // TODO: handle unresolved page better return m } - if (targetModule !== 'ROOT') relativePart = `${targetModule}${idCoordinateSeparator}${relativePart}` - relativePart = relativePart.replace(/\.adoc$/, '').replace(/[/.]/g, '-') if (fragment === resource.asciidoc.id) fragment = '' - const refid = fragment - ? `${relativePart}${idScopeSeparator}${fragment}` - : relativePart + (ReservedIdNames.includes(relativePart) ? idScopeSeparator : '') + const refid = generateId(resource.src, componentVersion, idCoordinateSeparator, idScopeSeparator, fragment).id if ( text && (assemblyModel.dropExplicitXrefText === 'always' || @@ -603,9 +566,7 @@ function mergeAsciiDoc ( const resource = files.find((it) => it.pub.url === url) if (resource) { if (resource.src.family === 'page' && pagesInOutline.has(resource.pub.url)) { - let refid = resource.src.relative.replace(/\.adoc$/, '').replace(/[/.]/g, '-') - if (refid.startsWith('./')) refid = topicPrefix + refid.slice(2) - if (resource.src.module !== 'ROOT') refid = `${resource.src.module}${idCoordinateSeparator}${refid}` + const refid = generateId(resource.src, componentVersion, idCoordinateSeparator, idScopeSeparator).id sectionTitle = `xref:${refid}[${navtitleAsciiDoc.replace(/\]/g, '\\]')}]` } else if (siteRoot) { sectionTitle = `${resolveLinkTarget(resource, siteRoot, pubRoot, linkRefStyle)}[${navtitleAsciiDoc.replace(/\]/g, '\\]')}]` @@ -842,4 +803,23 @@ function computeRelativeUrl (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 ({ component, module: mod, relative }, componentVersion, coordinateSep, scopeSep, fragment) { + let id = relative.replace(/\.adoc$/, '').replace(/[/.]/g, '-') + if (component !== componentVersion.name) { + id = [component, mod === 'ROOT' ? '' : mod, id].join(coordinateSep) + } else if (mod !== 'ROOT') { + 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 diff --git a/packages/assembler/lib/util/parse-resource-ref.js b/packages/assembler/lib/util/parse-resource-ref.js new file mode 100644 index 0000000000000000000000000000000000000000..461f1b4fffa58ac0c51160ffe495a3ddd0dbb897 --- /dev/null +++ b/packages/assembler/lib/util/parse-resource-ref.js @@ -0,0 +1,36 @@ +'use strict' + +function parseResourceRef (ref, ctx = {}, family = undefined, contentCatalog = undefined) { + const atIdx = ref.indexOf('@') + let firstColonIdx = ref.indexOf(':') + let component, version, module_ + if (~atIdx && (~firstColonIdx ? atIdx < firstColonIdx : true)) { + if ((version = ref.slice(0, atIdx)) === '_') version = '' + ref = ref.slice(atIdx + 1) + if (~firstColonIdx) firstColonIdx -= atIdx + 1 + } + const addColons = ~firstColonIdx ? (~ref.indexOf(':', firstColonIdx + 1) ? '' : ':') : '::' + const segments = (addColons + ref).split(':') + if ((component = segments[0])) { + module_ = segments[1] || 'ROOT' + version ??= contentCatalog?.getComponent(component)?.latest.version + } else { + component = ctx.component + version ??= ctx.version + module_ = segments[1] || ctx.module || 'ROOT' + } + let relative = segments.length > 3 ? segments.slice(2).join(':') : segments[2] + const dollarIdx = relative.indexOf('$') + if (~dollarIdx) { + family = relative.slice(0, dollarIdx) || family + relative = relative.slice(dollarIdx + 1) + } + if (relative.charAt() === '.' && relative.charAt(1) === '/') { + const ctxRelative = ctx.relative + const topic = ctxRelative ? ctxRelative.slice(0, (ctxRelative.lastIndexOf('/') + 1 || 1) - 1) : undefined + relative = (topic ? topic + '/' : '') + relative.slice(2) + } + return { component, version, module: module_, family, relative } +} + +module.exports = parseResourceRef diff --git a/packages/assembler/package.json b/packages/assembler/package.json index 9e4cf049f29380e7de72d5c5420d4d8d9a509362..8ea5e5bdea173a92f1dde9ba872c2477713be57f 100644 --- a/packages/assembler/package.json +++ b/packages/assembler/package.json @@ -27,6 +27,7 @@ ".": "./lib/index.js", "./filter-component-versions": "./lib/filter-component-versions.js", "./load-config": "./lib/load-config.js", + "./parse-resource-ref": "./lib/util/parse-resource-ref.js", "./produce-assembly-file": "./lib/produce-assembly-file.js", "./produce-assembly-files": "./lib/produce-assembly-files.js", "./select-mutable-attributes": "./lib/select-mutable-attributes.js" diff --git a/packages/assembler/test/produce-assembly-files-test.js b/packages/assembler/test/produce-assembly-files-test.js index 8a146901bde6ad6e22190266df5f838a4a961a01..8edf93f4f0f56c2504c9b6c8056636be58787162 100644 --- a/packages/assembler/test/produce-assembly-files-test.js +++ b/packages/assembler/test/produce-assembly-files-test.js @@ -151,7 +151,7 @@ describe('produceAssemblyFiles()', () => { await runScenario('scrub-ids', __dirname) }) - it('should scope pages from a different component', async () => { + it('should scope pages from a different component and link to them internally', async () => { await runScenario('cross-component', __dirname) }) diff --git a/packages/assembler/test/scenarios/cross-component/data.yml b/packages/assembler/test/scenarios/cross-component/data.yml index 59b5789fb7b5d8a90145cea17edb4d7b04c2ef4b..472c93185555aa3b0873a5325c6dbeb45a182cfb 100644 --- a/packages/assembler/test/scenarios/cross-component/data.yml +++ b/packages/assembler/test/scenarios/cross-component/data.yml @@ -13,7 +13,9 @@ files: contents - see xref:another-page.adoc[] + xref:another-page.adoc[] and xref:the-component::another-page.adoc[] are the same page + + xref:another-page.adoc[] and xref:other-component::another-page.adoc[] are different pages, but both are in this assembly - component: other-component relative: another-page.adoc contents: |- diff --git a/packages/assembler/test/scenarios/cross-component/expects/index.adoc b/packages/assembler/test/scenarios/cross-component/expects/index.adoc index d6b64f2b47f287401cb8bad1bea25473278ba1f4..75c1813cddcd3164b333218d8587927248ff5624 100644 --- a/packages/assembler/test/scenarios/cross-component/expects/index.adoc +++ b/packages/assembler/test/scenarios/cross-component/expects/index.adoc @@ -21,7 +21,9 @@ contents -see xref:another-page[] +xref:another-page[] and xref:another-page[] are the same page + +xref:another-page[] and xref:other-component::another-page[] are different pages, but both are in this assembly :docname: another-page :page-component-name: other-component diff --git a/packages/test-harness/lib/load-scenario.js b/packages/test-harness/lib/load-scenario.js index fba5302e6db85f8809e198b49ae6d9a5e921aa52..8b8c2f7971f34e52c7684e0032347110cf6f29e7 100644 --- a/packages/test-harness/lib/load-scenario.js +++ b/packages/test-harness/lib/load-scenario.js @@ -2,6 +2,7 @@ const { loadAsciiDoc, resolveAsciiDocConfig } = require('@antora/asciidoc-loader') const loadAssemblerConfig = require('@antora/assembler/load-config') +const parseResourceRef = require('@antora/assembler/parse-resource-ref') const fsp = require('node:fs/promises') const yaml = require('js-yaml') const ospath = require('node:path') @@ -68,18 +69,15 @@ class ContentCatalog { ) } - resolvePage (spec, ctx = {}) { - const src = parseResourceSpec(spec, ctx) + resolvePage (ref, ctx = {}) { + const src = parseResourceRef(ref, ctx, 'page') if (src.component) src.version ??= this.getComponent(src.component).latest.version - return ( - this.getById(Object.assign(src, { family: 'page' })) ?? this.getById(Object.assign(src, { family: 'alias' }))?.rel - ) + return this.getById(src) ?? this.getById(Object.assign(src, { family: 'alias' }))?.rel } - resolveResource (spec, ctx = {}, defaultFamily = undefined) { - const src = parseResourceSpec(spec, ctx) + resolveResource (ref, ctx = {}, defaultFamily = undefined) { + const src = parseResourceRef(ref, ctx, defaultFamily) if (src.component) src.version ??= this.getComponent(src.component).latest.version - src.family ??= defaultFamily || 'page' return this.getById(src) } @@ -287,28 +285,6 @@ function expandNavigation (item, contentCatalog, componentVersion) { return item } -function parseResourceSpec (spec, { component, version, module: module_ = 'ROOT' }) { - const parts = spec.split(':') - const segments = parts.slice(0, 2) - if (parts.length > 2) segments.push(parts.slice(2).join(':')) - let family, relative - if (segments.length === 1) { - relative = segments[0] - } else if (segments.length === 2) { - ;[module_, relative] = segments - } else { - if (!segments[1]) segments[1] = 'ROOT' - ;[component, module_, relative] = segments - if (~component.indexOf('@')) { - ;[version, component] = component.split('@') - } else { - version = undefined - } - } - if (~relative.indexOf('$')) [family, relative] = relative.split('$') - return { component, version, module: module_, family, relative } -} - function toTitle (string) { return string .split('-')