PageSourceSearch

https://journeywest.com/js/jw-alt-text-loader.js?v=20260917

js journeywest.com collected 2026-10-09 22:41:47 UTC 10,512 bytes, 286 lines download raw bytes

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.