140 lines
4.8 KiB
JavaScript
140 lines
4.8 KiB
JavaScript
/* transclude.js — Client-side lazy transclusion.
|
|
*
|
|
* Authored in Markdown as a standalone line:
|
|
* {{slug}} — embed full body of /slug.html
|
|
* {{slug#section-id}} — embed one section by heading id
|
|
* {{path/to/page}} — sub-path pages work the same way
|
|
*
|
|
* The Haskell preprocessor (Filters.Transclusion) converts these at build
|
|
* time to placeholder divs:
|
|
* <div class="transclude" data-src="/slug.html"
|
|
* data-section="section-id"></div>
|
|
*
|
|
* This script finds those divs, fetches the target page, extracts the
|
|
* requested content, rewrites cross-page fragment hrefs, injects the
|
|
* content inline, and retriggers layout-dependent JS (sidenotes, collapse).
|
|
*/
|
|
|
|
(function () {
|
|
'use strict';
|
|
|
|
/* Shared fetch cache — one network request per URL regardless of how
|
|
* many transclusions reference the same page. */
|
|
var cache = {};
|
|
|
|
function fetchPage(url) {
|
|
if (!cache[url]) {
|
|
cache[url] = fetch(url).then(function (r) {
|
|
if (!r.ok) throw new Error('HTTP ' + r.status);
|
|
return r.text();
|
|
});
|
|
}
|
|
return cache[url];
|
|
}
|
|
|
|
function parseDoc(html) {
|
|
return new DOMParser().parseFromString(html, 'text/html');
|
|
}
|
|
|
|
/* Extract a named section: the heading element with id=sectionId plus
|
|
* all following siblings until the next heading at the same or higher
|
|
* level (lower number), or end of parent. */
|
|
function extractSection(doc, sectionId) {
|
|
var anchor = doc.getElementById(sectionId);
|
|
if (!anchor) return null;
|
|
|
|
var level = parseInt(anchor.tagName[1], 10);
|
|
if (!level) return null;
|
|
|
|
var nodes = [anchor.cloneNode(true)];
|
|
var el = anchor.nextElementSibling;
|
|
|
|
while (el) {
|
|
if (/^H[1-6]$/.test(el.tagName) &&
|
|
parseInt(el.tagName[1], 10) <= level) break;
|
|
nodes.push(el.cloneNode(true));
|
|
el = el.nextElementSibling;
|
|
}
|
|
|
|
return nodes.length ? nodes : null;
|
|
}
|
|
|
|
/* Extract the full contents of #markdownBody. */
|
|
function extractBody(doc) {
|
|
var body = doc.getElementById('markdownBody');
|
|
if (!body) return null;
|
|
var nodes = Array.from(body.children).map(function (el) {
|
|
return el.cloneNode(true);
|
|
});
|
|
return nodes.length ? nodes : null;
|
|
}
|
|
|
|
/* Rewrite href="#fragment" → href="srcUrl#fragment" so in-page anchor
|
|
* links from the source page remain valid when embedded elsewhere. */
|
|
function rewriteFragmentHrefs(nodes, srcUrl) {
|
|
nodes.forEach(function (node) {
|
|
node.querySelectorAll('a[href^="#"]').forEach(function (a) {
|
|
a.setAttribute('href', srcUrl + a.getAttribute('href'));
|
|
});
|
|
});
|
|
}
|
|
|
|
/* After injection, retrigger layout-dependent subsystems. */
|
|
function reinitFragment(container) {
|
|
/* sidenotes.js repositions on resize — dispatch to trigger it. */
|
|
window.dispatchEvent(new Event('resize'));
|
|
|
|
/* collapse.js exposes reinitCollapse for newly added headings. */
|
|
if (typeof window.reinitCollapse === 'function') {
|
|
window.reinitCollapse(container);
|
|
}
|
|
|
|
/* gallery.js can expose reinitGallery when needed. */
|
|
if (typeof window.reinitGallery === 'function') {
|
|
window.reinitGallery(container);
|
|
}
|
|
}
|
|
|
|
function loadTransclusion(el) {
|
|
var src = el.dataset.src;
|
|
var section = el.dataset.section || null;
|
|
if (!src) return;
|
|
|
|
el.classList.add('transclude--loading');
|
|
|
|
fetchPage(src)
|
|
.then(function (html) {
|
|
var doc = parseDoc(html);
|
|
var nodes = section
|
|
? extractSection(doc, section)
|
|
: extractBody(doc);
|
|
|
|
if (!nodes) {
|
|
el.classList.replace('transclude--loading', 'transclude--error');
|
|
el.textContent = '[transclusion not found: '
|
|
+ src + (section ? '#' + section : '') + ']';
|
|
return;
|
|
}
|
|
|
|
rewriteFragmentHrefs(nodes, src);
|
|
|
|
var wrapper = document.createElement('div');
|
|
wrapper.className = 'transclude--content';
|
|
nodes.forEach(function (n) { wrapper.appendChild(n); });
|
|
|
|
el.classList.replace('transclude--loading', 'transclude--loaded');
|
|
el.appendChild(wrapper);
|
|
|
|
reinitFragment(el);
|
|
})
|
|
.catch(function (err) {
|
|
el.classList.replace('transclude--loading', 'transclude--error');
|
|
console.warn('transclude: failed to load', src, err);
|
|
});
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', function () {
|
|
document.querySelectorAll('div.transclude').forEach(loadTransclusion);
|
|
});
|
|
}());
|