1/* eslint-disable import/no-cycle */ 2/* eslint-disable max-classes-per-file */ 3/* eslint-disable no-return-assign */ 4/* eslint-disable no-restricted-syntax */ 5import { createTag, getConfig, loadArea, localizeLink } from '../../milo-utils/utils.js'; 6 7const fragMap = {}; 8 9const removeHash = (url) => { 10 const urlNoHash = url.split('#')[0]; 11 return url.includes('#_dnt') ? `${urlNoHash}#_dnt` : urlNoHash; 12}; 13 14const isCircularRef = (href) => [...Object.values(fragMap)] 15 .some((tree) => { 16 const node = tree.find(href); 17 return node ? !(node.isLeaf) : false; 18 }); 19 20const updateFragMap = (fragment, a, href) => { 21 const fragLinks = [...fragment.querySelectorAll('a')] 22 .filter((link) => localizeLink(link.href).includes('/fragments/')); 23 if (!fragLinks.length) return; 24 25 if (document.body.contains(a)) { // is fragment on page (not nested) 26 // eslint-disable-next-line no-use-before-define 27 fragMap[href] = new Tree(href); 28 fragLinks.forEach((link) => fragMap[href].insert(href, localizeLink(removeHash(link.href)))); 29 } else { 30 Object.values(fragMap).forEach((tree) => { 31 if (tree.find(href)) { 32 fragLinks.forEach((link) => tree.insert(href, localizeLink(removeHash(link.href)))); 33 } 34 }); 35 } 36}; 37 38const setManifestIdOnChildren = (sections, manifestId) => { 39 [...sections[0].children].forEach( 40 (child) => (child.dataset.manifestId = manifestId), 41 ); 42}; 43 44const insertInlineFrag = (sections, a, relHref) => { 45 // Inline fragments only support one section, other sections are ignored 46 const fragChildren = [...sections[0].children]; 47 fragChildren.forEach((child) => child.setAttribute('data-path', relHref)); 48 if (a.parentElement.nodeName === 'DIV' && !a.parentElement.attributes.length) { 49 a.parentElement.replaceWith(...fragChildren); 50 } else { 51 a.replaceWith(...fragChildren); 52 } 53}; 54 55function replaceDotMedia(path, doc) { 56 const resetAttributeBase = (tag, attr) => { 57 doc.querySelectorAll(`${tag}[${attr}^="./media_"]`).forEach((el) => { 58 el[attr] = new URL(el.getAttribute(attr), new URL(path, window.location)).href; 59 }); 60 }; 61 resetAttributeBase('img', 'src'); 62 resetAttributeBase('source', 'srcset'); 63} 64 65export default async function init(a) { 66 const { expFragments, decorateArea, mep } = getConfig(); 67 let relHref = localizeLink(a.href); 68 let inline = false; 69 70 if (a.parentElement?.nodeName === 'P') { 71 const children = a.parentElement.childNodes; 72 const div = createTag('div'); 73 for (const attr of a.parentElement.attributes) div.setAttribute(attr.name, attr.value); 74 a.parentElement.replaceWith(div); 75 div.append(...children); 76 } 77 78 if (a.href.includes('#_inline')) { 79 inline = true; 80 a.href = a.href.replace('#_inline', ''); 81 relHref = relHref.replace('#_inline', ''); 82 } 83 84 const path = new URL(a.href).pathname; 85 if (expFragments?.[path] && mep) { 86 relHref = mep.handleFragmentCommand(expFragments[path], a); 87 if (!relHref) return; 88 } 89 90 if (isCircularRef(relHref)) { 91 window.lana?.log(`ERROR: Fragment Circular Reference loading ${a.href}`); 92 return; 93 } 94 95 const { customFetch } = await import('../../milo-utils/helpers.js'); 96 const resp = await customFetch({ resource: `${a.href}.plain.html`, withCacheRules: true }) 97 .catch(() => ({})); 98 99 if (!resp.ok) { 100 window.lana?.log(`Could not get fragment: ${a.href}.plain.html`); 101 return; 102 } 103 104 const html = await resp.text(); 105 const doc = new DOMParser().parseFromString(html, 'text/html'); 106 replaceDotMedia(a.href, doc); 107 if (decorateArea) decorateArea(doc, { fragmentLink: a }); 108 109 const sections = doc.querySelectorAll('body > div'); 110 111 if (!sections.length) { 112 window.lana?.log(`Could not make fragment: ${a.href}.plain.html`); 113 return; 114 } 115 116 const fragment = createTag('div', { class: 'fragment', 'data-path': relHref }); 117 118 if (!inline) { 119 fragment.append(...sections); 120 } 121 122 updateFragMap(fragment, a, relHref); 123 124 if (a.dataset.manifestId) { 125 if (inline) { 126 setManifestIdOnChildren(sections, a.dataset.manifestId); 127 } else { 128 fragment.dataset.manifestId = a.dataset.manifestId; 129 } 130 } 131 132 if (inline) { 133 insertInlineFrag(sections, a, relHref); 134 } else { 135 a.parentElement.replaceChild(fragment, a); 136 await loadArea(fragment); 137 } 138} 139 140class Node { 141 constructor(key, value = key, parent = null) { 142 this.key = key; 143 this.value = value; 144 this.parent = parent; 145 this.children = []; 146 } 147 148 get isLeaf() { 149 return this.children.length === 0; 150 } 151} 152 153export class Tree { 154 constructor(key, value = key) {
155 this.root = new Node(key, value); 156 } 157 158 * traverse(node = this.root) { 159 yield node; 160 if (node.children.length) { 161 for (const child of node.children) { 162 yield* this.traverse(child); 163 } 164 } 165 } 166 167 insert(parentNodeKey, key, value = key) { 168 for (const node of this.traverse()) { 169 if (node.key === parentNodeKey) { 170 node.children.push(new Node(key, value, node)); 171 return true; 172 } 173 } 174 return false; 175 } 176 177 remove(key) { 178 for (const node of this.traverse()) { 179 const filtered = node.children.filter((c) => c.key !== key); 180 if (filtered.length !== node.children.length) { 181 node.children = filtered; 182 return true; 183 } 184 } 185 return false; 186 } 187 188 find(key) { 189 for (const node of this.traverse()) { 190 if (node.key === key) return node; 191 } 192 return undefined; 193 } 194}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.