diff --git a/docs-site/extensions/antora-static-nav/lib/index.js b/docs-site/extensions/antora-static-nav/lib/index.js index 67ff93c58..32ac8e4bc 100644 --- a/docs-site/extensions/antora-static-nav/lib/index.js +++ b/docs-site/extensions/antora-static-nav/lib/index.js @@ -87,6 +87,44 @@ function cleanNav (nav) { .replace(/(]+class=["'][^"']*?)\s+is-active\b/g, '$1') } +// --------------------------------------------------------------------------- +// Href/src rebasing +// --------------------------------------------------------------------------- + +const URL_REBASE_RX = /(\s(?:href|src)=)(["'])([^"']*)\2/g +const ABSOLUTE_OR_FRAGMENT_RX = /^(?:[a-z][a-z0-9+.-]*:|\/\/|\/|#|data:|mailto:)/i + +/** + * Rewrite a single href/src value so it is relative to the component@version + * root rather than to the sample page's directory. + * + * Antora emits nav URLs as page-relative paths. When the captured nav is + * reused on pages at a different depth, these page-relative paths break. + * Normalising to component-root-relative makes the URLs portable; the loader + * then prepends each page's own back-to-root prefix at injection time. + */ +function rebaseUrl (value, sampleDirRelToRoot) { + if (!value) return value + if (ABSOLUTE_OR_FRAGMENT_RX.test(value)) return value + const base = sampleDirRelToRoot ? '/' + sampleDirRelToRoot + '/' : '/' + const resolved = path.posix.normalize(base + value) + return resolved.replace(/^\/+/, '') +} + +/** + * Rebase every href/src in the nav HTML to be component-root-relative. + * `sampleDirRelToRoot` is the sample page's directory expressed relative to + * the component@version output root (e.g. "" for a root-level page, + * "source" for source/foo.html, "module/sub" for nested pages). + */ +function rebaseNav (navHtml, sampleDirRelToRoot) { + const norm = sampleDirRelToRoot && sampleDirRelToRoot !== '.' ? sampleDirRelToRoot : '' + return navHtml.replace(URL_REBASE_RX, (m, attr, q, val) => { + const next = rebaseUrl(val, norm) + return next === val ? m : `${attr}${q}${next}${q}` + }) +} + // --------------------------------------------------------------------------- // Output builders // --------------------------------------------------------------------------- @@ -94,19 +132,27 @@ function cleanNav (nav) { /** * Build the content of _static_nav.js. * - * Injects the nav synchronously via currentScript.parentNode.insertBefore — - * no document.write(), no fetch(). JSON.stringify handles all HTML escaping. + * Exposes window.__antoraStaticNav(prefix). The per-page inline script calls + * it with the page's relative path back to the component@version root, so a + * single shared nav can serve pages at any depth. The injected nav is placed + * just before the calling script, where the original nav block lived. */ function buildNavJs (navHtml) { const navJson = JSON.stringify(navHtml) return ( - '(function(){' + - 'var s=document.currentScript||document.scripts[document.scripts.length-1];' + + 'window.__antoraStaticNav=function(prefix){' + + 'var s=document.currentScript;' + + 'if(!s){var ss=document.scripts;s=ss[ss.length-1];}' + + 'if(!s)return;' + + `var html=${navJson};` + + 'if(prefix){html=html.replace(/(\\s(?:href|src)=)(["\'])([^"\']*)\\2/g,function(m,a,q,v){' + + 'if(/^(?:[a-z][a-z0-9+.-]*:|\\/\\/|\\/|#|data:|mailto:)/i.test(v))return m;' + + 'return a+q+prefix+v+q;});}' + 'var t=document.createElement("div");' + - `t.innerHTML=${navJson};` + + 't.innerHTML=html;' + 'var n=t.firstChild;' + 'if(n)s.parentNode.insertBefore(n,s);' + - '})();\n' + '};\n' ) } @@ -114,12 +160,15 @@ function buildNavJs (navHtml) { * Build the HTML fragment that replaces a page's embedded nav block. * * Two