1/* 2 * JourneyWest Legacy ALT/Text Loader 3 * Version: 2026-09-18 4 * 5 * Purpose: 6 * - Reads alt_text.json files that already live beside JourneyWest images. 7 * - Fills empty/missing ALT text on legacy pages. 8 * - Also fills TITLE so the same description appears on mouse-over. 9 * - Preserves any meaningful ALT or TITLE already present. 10 * - Works on localhost/XAMPP and journeywest.com. 11 */ 12(function () { 13 "use strict"; 14 15 if (window.JOURNEYWEST_LEGACY_ALT_LOADER_LOADED) return; 16 window.JOURNEYWEST_LEGACY_ALT_LOADER_LOADED = true; 17 window.JOURNEYWEST_LEGACY_ALT_LOADER_VERSION = "2026-09-18"; 18 19 const jsonCache = new Map(); 20 21 function toUrl(value) { 22 try { 23 return new URL(value, window.location.href); 24 } catch (e) { 25 return null; 26 } 27 } 28 29 function decodeFilename(urlObj) { 30 const raw = (urlObj.pathname.split("/").pop() || "").split("?")[0]; 31 try { 32 return decodeURIComponent(raw); 33 } catch (e) { 34 return raw; 35 } 36 } 37 38 function folderPath(urlObj) { 39 return urlObj.pathname.replace(/\/[^\/]*$/, "") || "/"; 40 } 41 42 async function loadJson(pathname) { 43 if (jsonCache.has(pathname)) return jsonCache.get(pathname); 44 45 // Always request the JSON from the current page's origin. This matters 46 // when an image itself is served from media.journeywest.com. 47 let jsonUrl; 48 try { 49 jsonUrl = new URL(pathname, window.location.origin).href; 50 } catch (e) { 51 jsonCache.set(pathname, null); 52 return null; 53 } 54 55 try { 56 const response = await fetch(jsonUrl, { cache: "no-cache" }); 57 if (!response.ok) { 58 jsonCache.set(pathname, null); 59 return null; 60 } 61 const data = await response.json(); 62 const usable = data && typeof data === "object" ? data : null; 63 jsonCache.set(pathname, usable); 64 return usable; 65 } catch (e) { 66 jsonCache.set(pathname, null); 67 return null; 68 } 69 } 70 71 function lookupAlt(map, filename) { 72 if (!map || !filename) return ""; 73 74 if (Object.prototype.hasOwnProperty.call(map, filename)) { 75 const exact = String(map[filename] ?? "").trim(); 76 if (exact) return exact; 77 } 78 79 const wanted = filename.toLowerCase(); 80 for (const key in map) { 81 if (!Object.prototype.hasOwnProperty.call(map, key)) continue; 82 if (String(key).toLowerCase() === wanted) { 83 const value = String(map[key] ?? "").trim(); 84 if (value) return value; 85 } 86 } 87 return ""; 88 } 89 90 function addUnique(arr, value) { 91 if (value && !arr.includes(value)) arr.push(value); 92 } 93 94 function jsonCandidatesForImage(imageUrl) { 95 const candidates = []; 96 const folder = folderPath(imageUrl); 97 98 // Normal case: JSON sits beside the image. 99 addUnique(candidates, folder.replace(/\/$/, "") + "/alt_text.json"); 100 101 // Common JourneyWest gallery/helper subfolders may share the parent JSON. 102 const helperFolderPattern = /\/(?:thumbnails?|thumbs?|slides|res|fancybox(?:full)?|photoswipe(?:original)?|spotlight|optimized)$/i; 103 let parent = folder; 104 while (helperFolderPattern.test(parent)) { 105 parent = parent.replace(helperFolderPattern, ""); 106 addUnique(candidates, parent.replace(/\/$/, "") + "/alt_text.json"); 107 } 108 109 // Some generated thumbnails live one level below a photoswipe folder. 110 if (/\/photoswipe\/thumbnails?$/i.test(folder)) { 111 addUnique(candidates, folder.replace(/\/thumbnails?$/i, "") + "/alt_text.json"); 112 } 113 114 // Early JourneyWest pages use the cumulative root images database. 115 addUnique(candidates, "/images/alt_text.json"); 116 117 return candidates; 118 } 119 120 function mediaUrlForImage(img) { 121 // For ordinary legacy images, src/currentSrc is the correct lookup. 122 // If a gallery anchor links to a full-size image while the img is a
123 // thumbnail, prefer the anchor so the filename matches the JSON entry. 124 const anchor = img.closest ? img.closest("a[href]") : null; 125 if (anchor) { 126 const href = (anchor.getAttribute("href") || "").trim(); 127 if (href && href !== "#" && !/^javascript:/i.test(href)) { 128 const anchorUrl = toUrl(href); 129 if (anchorUrl && /\.(?:jpe?g|png|gif|webp|avif|bmp)(?:$|\?)/i.test(anchorUrl.href)) { 130 return href; 131 } 132 } 133 } 134 135 return img.currentSrc || img.getAttribute("src") || img.getAttribute("data-src") || ""; 136 } 137 138 async function findAltForImage(img) { 139 const mediaValue = mediaUrlForImage(img); 140 if (!mediaValue) return ""; 141 142 const mediaUrl = toUrl(mediaValue); 143 if (!mediaUrl) return ""; 144 145 const filename = decodeFilename(mediaUrl); 146 if (!filename) return ""; 147 148 const candidates = jsonCandidatesForImage(mediaUrl); 149 for (const jsonPath of candidates) { 150 const map = await loadJson(jsonPath); 151 const alt = lookupAlt(map, filename); 152 if (alt) return alt; 153 } 154 155 // If an anchor pointed to the large image but JSON only knows the 156 // thumbnail filename, make one second attempt using the IMG src. 157 const directSrc = img.currentSrc || img.getAttribute("src") || img.getAttribute("data-src") || ""; 158 if (directSrc && directSrc !== mediaValue) { 159 const directUrl = toUrl(directSrc); 160 if (directUrl) { 161 const directFilename = decodeFilename(directUrl); 162 for (const jsonPath of jsonCandidatesForImage(directUrl)) { 163 const map = await loadJson(jsonPath); 164 const alt = lookupAlt(map, directFilename); 165 if (alt) return alt; 166 } 167 } 168 } 169 170 return ""; 171 } 172 173 function isDecorativeOrUtilityImage(img) { 174 const src = (img.currentSrc || img.getAttribute("src") || "").toLowerCase(); 175 176 // Do not turn obvious spacers/tracking pixels into verbose hover text. 177 if (/spacer|shim|clear\.gif|transparent|pixel\.gif|1x1/i.test(src)) return true; 178 179 // JourneyWest shared menu/background folders are decorative site chrome, 180 // not journal photographs. They intentionally do not have per-folder 181 // alt_text.json files, so never probe those folders. 182 if (/\/images\/!index\//i.test(src)) return true; 183 if (/\/images\/!galleryindeximages\//i.test(src)) return true; 184 if (/\/images\/!gallerymountainssummer\//i.test(src)) return true; 185 186 // Backstretch creates IMG elements dynamically for page backgrounds. 187 // Those are decorative backgrounds and should never trigger ALT/JSON lookups. 188 if (img.closest && img.closest(".backstretch")) return true; 189 190 // Explicitly decorative images should remain decorative. 191 if ((img.getAttribute("role") || "").toLowerCase() === "presentation") return true; 192 if ((img.getAttribute("aria-hidden") || "").toLowerCase() === "true") return true; 193 194 return false; 195 } 196 197 async function processImage(img) { 198 if (!img || img.nodeType !== 1 || img.tagName.toLowerCase() !== "img") return; 199 if (img.dataset && img.dataset.jwAltChecked === "1") return; 200 if (img.dataset) img.dataset.jwAltChecked = "1"; 201 202 if (isDecorativeOrUtilityImage(img)) return; 203 204 const existingAlt = (img.getAttribute("alt") || "").trim(); 205 const existingTitle = (img.getAttribute("title") || "").trim(); 206 207 // Never overwrite meaningful ALT text already present in the page. 208 if (existingAlt) { 209 // JourneyWest preference: if TITLE is empty, reuse the good ALT 210 // so mouse-over text is available without changing ALT semantics. 211 if (!existingTitle) img.setAttribute("title", existingAlt); 212 return; 213 } 214 215 const alt = await findAltForImage(img); 216 if (!alt) return; 217 218 img.setAttribute("alt", alt); 219 if (!existingTitle) img.setAttribute("title", alt); 220 221 const anchor = img.closest ? img.closest("a[href]") : null; 222 if (anchor) { 223 if (!(anchor.getAttribute("title") || "").trim()) anchor.setAttribute("title", alt); 224 if (!(anchor.getAttribute("aria-label") || "").trim()) anchor.setAttribute("aria-label", alt); 225 if (!(anchor.getAttribute("data-alt") || "").trim()) anchor.setAttribute("data-alt", alt); 226 } 227 } 228 229 function legacyImageSelector() { 230 return [ 231 "#wrapper-content img[src]", 232 "#content img[src]", 233 "#main img[src]", 234 ".article_inner img[src]", 235 ".content img[src]" 236 ].join(", "); 237 } 238 239 async function applyAltText(root) { 240 root = root || document; 241 242 let images = Array.from(root.querySelectorAll(legacyImageSelector())); 243 244 // Some very old pages have no standard content wrapper. 245 if (!images.length) { 246 images = Array.from(root.querySelectorAll('img[src*="images/"], img[src^="/images/"]')); 247 } 248 249 for (const img of images) { 250 await processImage(img); 251 } 252 } 253 254 window.jwApplyLegacyAltText = applyAltText; 255 256 function start() { 257 applyAltText(document); 258 259 // Handle galleries/scripts that add images after the initial page load. 260 const observer = new MutationObserver(function (mutations) { 261 for (const mutation of mutations) { 262 for (const node of mutation.addedNodes) { 263 if (!(node instanceof Element)) continue; 264 265 if (node.matches && node.matches("img[src]")) { 266 processImage(node); 267 } 268 if (node.querySelectorAll) { 269 node.querySelectorAll("img[src]").forEach(processImage); 270 } 271 } 272 } 273 }); 274 275 observer.observe(document.documentElement, { 276 childList: true, 277 subtree: true 278 }); 279 } 280 281 if (document.readyState === "loading") { 282 document.addEventListener("DOMContentLoaded", start, { once: true }); 283 } else { 284 start(); 285 } 286})();
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.