Greasy Fork

Greasy Fork is available in English.

Nitan Airport Flair

Enhances webpages by identifying airport and multi-airport codes, styling them, and adding interactive tooltips/hyperlinks. Dismiss reverts instances.

当前为 2025-05-15 提交的版本,查看 最新版本

// ==UserScript==
// @name         Nitan Airport Flair
// @namespace    http://tampermonkey.net/
// @version      0.5.2
// @description  Enhances webpages by identifying airport and multi-airport codes, styling them, and adding interactive tooltips/hyperlinks. Dismiss reverts instances.
// @author       s5kf
// @license      CC BY-NC-ND 4.0; https://creativecommons.org/licenses/by-nc-nd/4.0/
// @match        *://www.uscardforum.com/*
// @resource     airportJsonData https://raw.githubusercontent.com/s5kf/airport_flair/main/airports_filtered.json
// @resource     multiAirportJsonData https://raw.githubusercontent.com/s5kf/airport_flair/main/multi_airport_data.json
// @grant        GM_addStyle
// @grant        GM_getResourceText
// @run-at       document-idle
// @supportURL   https://github.com/s5kf/airport_flair/issues
// ==/UserScript==

(function() {
    'use strict';

    let airportData = {}; // To store single airport data keyed by IATA code
    let multiAirportData = {}; // To store multi-airport area data
    let observer = null; // To store the MutationObserver instance

    // --- Constants (moved up for early initialization) ---
    const iataRegex = /\b([A-Za-z]{3})\b/g;
    const processedMark = 'airport-flair-processed';
    const flairTag = 'airport-flair-tag'; // Class for the flair span itself
    const potentialFlairClass = 'potential-airport-code'; // New class for potential flairs
    const multiAirportFlairClass = 'multi-airport-flair'; // New class for multi-airport flairs
    const COMMON_TLA_BLOCKLIST = new Set([
        "CEO", "CFO", "COO", "CTO", "CIO", "CMO", "HRD", // Common C-suite and department heads
        "GDP", "GNP", "ROI", "KPI", "ETA", "FAQ", "DIY", "AKA", // Common business & general acronyms
        "USB", "CPU", "GPU", "RAM", "SSD", "HDD", "OSX", "IOS", // Tech acronyms
        "LOL", "OMG", "BTW", "FYI", "IMO",  "BRB", // Internet slang
        "USA", "UKG", "CAN", // Note: UKG for United Kingdom, CAN for Canada if they conflict. Add specific airport codes if they are also common TLAs.
        "ETA", "ETC", "INC", "LTD", "LLC", "DIY", "FAQ", "PDF", "XML", "DOC",
        "API", "URL", "WWW", "CSS",
        "ESG", "VIX", "AOC"

        // Add more as needed, ensure they are uppercase
    ]);

    // --- Load Data from @resource ---
    function initializeData() {
        let mainDataLoaded = false;
        let multiDataLoaded = false;

        // Load Main Airport Data
        try {
            console.log("[Airport Flair] Attempting to load main airport data from @resource...");
            const airportJsonDataString = GM_getResourceText("airportJsonData");
            if (!airportJsonDataString) {
                console.error("[Airport Flair] Failed to get main airport data. GM_getResourceText returned empty.");
            } else {
                const sanitizedJsonDataString = airportJsonDataString
                    .replace(/: Infinity,/g, ": null,")
                    .replace(/: Infinity}/g, ": null}")
                    .replace(/: NaN,/g, ": null,")
                    .replace(/: NaN}/g, ": null}");
                const data = JSON.parse(sanitizedJsonDataString);
                data.forEach(airport => {
                    if (airport.iata_code) {
                        airportData[airport.iata_code.toUpperCase()] = airport;
                    }
                });
                console.log("[Airport Flair] Main airport data loaded and processed:", Object.keys(airportData).length, "entries");
                mainDataLoaded = true;
            }
        } catch (e) {
            console.error("[Airport Flair] Error loading or parsing main airport data:", e);
            // Optional: log airportJsonDataString snippet if needed
        }

        // Load Multi-Airport Data
        try {
            console.log("[Airport Flair] Attempting to load multi-airport data from @resource...");
            const multiAirportJsonDataString = GM_getResourceText("multiAirportJsonData");
            if (!multiAirportJsonDataString) {
                console.error("[Airport Flair] Failed to get multi-airport data. GM_getResourceText returned empty.");
            } else {
                multiAirportData = JSON.parse(multiAirportJsonDataString); // Assuming this JSON is clean
                console.log("[Airport Flair] Multi-airport data loaded and processed:", Object.keys(multiAirportData).length, "entries");
                multiDataLoaded = true;
            }
        } catch (e) {
            console.error("[Airport Flair] Error loading or parsing multi-airport data:", e);
            // Optional: log multiAirportJsonDataString snippet if needed
        }

        return mainDataLoaded; // Script functionality primarily depends on main airport data for flags etc.
                               // Multi-airport data is supplementary.
    }

    // Initialize data and then process the page
    if (initializeData()) {
        processPage();
    } else {
        console.error("[Airport Flair] Script will not run effectively due to critical data loading issues (main airport data).");
    }

    // --- CSS Styles ---
    function getDynamicStyles() {
        const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
        const htmlElement = document.documentElement;
        const bodyElement = document.body;

        let isLikelyDarkMode = prefersDark ||
                               htmlElement.classList.contains('dark') ||
                               bodyElement.classList.contains('dark') ||
                               htmlElement.classList.contains('dark-mode') || // Added common alternative
                               bodyElement.classList.contains('dark-mode');   // Added common alternative

        // Get computed background colors
        const htmlBgColor = getComputedStyle(htmlElement).backgroundColor;
        const bodyBgColor = getComputedStyle(bodyElement).backgroundColor;

        // Function to check if a CSS color string represents a dark color
        function isColorDark(colorString) {
            if (!colorString || colorString === 'transparent' || colorString === 'rgba(0, 0, 0, 0)') return false;
            if (colorString === 'rgb(0, 0, 0)' || colorString === '#000000') return true; // Pure black
            try {
                const rgbMatch = colorString.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
                if (rgbMatch) {
                    const r = parseInt(rgbMatch[1], 10);
                    const g = parseInt(rgbMatch[2], 10);
                    const b = parseInt(rgbMatch[3], 10);
                    // Heuristic: if average intensity is low, or all components are relatively low
                    if ((r + g + b) / 3 < 75 || (r < 100 && g < 100 && b < 100)) {
                        return true;
                    }
                }
            } catch (e) {
                // Ignore parsing errors, default to not dark for this check
            }
            return false;
        }

        if (!isLikelyDarkMode) {
            // If no class or media query indicated dark mode, check computed background colors
            if (isColorDark(bodyBgColor) || isColorDark(htmlBgColor)) {
                isLikelyDarkMode = true;
            }
        }

        let flairBgColor = '#eff3f4'; // Default light mode

        if (isLikelyDarkMode) {
            // Now, differentiate between dim and lights out if we've determined it's dark
            // Prioritize body background for "lights out" check, then html
            if (bodyBgColor === 'rgb(0, 0, 0)' || bodyBgColor === '#000000' ||
                htmlBgColor === 'rgb(0, 0, 0)' || htmlBgColor === '#000000') {
                flairBgColor = '#202327'; // Lights out
            } else {
                flairBgColor = '#273440'; // Dim mode (default dark)
            }
        }

        return `
            .${flairTag} {
                display: inline-flex;
                align-items: center;
                vertical-align: baseline;
                font-family: 'Fira Code', 'Roboto Mono', monospace;
                padding: 1px 4px; /* Reduced padding */
                color: #e6c07b; /* Light gold text */
                background-color: ${flairBgColor};
                border-radius: 3px;
                text-decoration: none; /* For the anchor tag */
                margin: 0 1px; /* Small margin to prevent touching adjacent text */
                position: relative; /* For absolute positioning of the dismiss button */
            }
            .${flairTag} img.country-flag {
                width: 16px;
                height: 12px;
                margin-left: 4px; /* Switched from margin-right and slightly increased for balance */
                vertical-align: middle;
            }
            .${flairTag} .dismiss-flair {
                position: absolute;
                top: -6px;
                right: -6px;
                width: 16px;
                height: 16px;
                line-height: 16px;
                text-align: center;
                font-size: 14px;
                font-weight: bold;
                background-color: rgba(74, 74, 74, 0.6); /* Translucent background */
                color: #ffffff;
                border-radius: 50%;
                cursor: pointer;
                opacity: 0;
                /* Only transition opacity and background-color for smoothness */
                transition: opacity 0.15s ease-in-out, background-color 0.15s ease-in-out;
                pointer-events: none; /* Prevent interaction when hidden */
                z-index: 10;
            }
            .${flairTag}:hover .dismiss-flair {
                opacity: 0.85; /* Default visible opacity */
                pointer-events: auto; /* Allow interaction when visible */
            }
            .${flairTag} .dismiss-flair:hover {
                opacity: 1; /* Full opacity on button hover */
                background-color: rgba(51, 51, 51, 0.85); /* Darker, slightly more opaque on hover */
            }
            .${potentialFlairClass} {
                text-decoration: underline dotted rgba(128, 128, 128, 0.7);
                cursor: help;
                /* Ensure it doesn't pick up parent link styles if it's inside an <a> not yet processed */
                color: inherit;
            }
            .${potentialFlairClass}:hover {
                text-decoration-color: rgba(100, 100, 100, 1); /* Darker underline on hover */
                background-color: rgba(200, 200, 200, 0.1); /* Very subtle hover background */
            }
            /* No specific hover title CSS needed if using default browser title attribute */

            /* Styles for Multi-Airport Flairs */
            .${multiAirportFlairClass} {
                display: inline-flex;
                align-items: center;
                vertical-align: baseline;
                font-family: 'Fira Code', 'Roboto Mono', monospace;
                padding: 1px 4px;
                color: #d19a66; /* Slightly different text color, e.g., a bit more orange */
                background-color: #3a3d41; /* Different background for dark mode by default */
                border-radius: 3px;
                text-decoration: none;
                margin: 0 1px;
                position: relative;
            }
            /* Dynamic background for multi-airport flairs based on theme */
            /* Light mode for multi-airport */
            html:not(.dark) body:not(.dark) .${multiAirportFlairClass},
            body:not([style*="background-color: rgb(0, 0, 0)"]):not([style*="background-color: #000"]) .${multiAirportFlairClass} {
                background-color: #e0e0e0; /* Lighter grey for light mode */
                color: #8c5a32;
            }
            html.dark .${multiAirportFlairClass},
            body.dark .${multiAirportFlairClass},
            body[style*="background-color: rgb(0, 0, 0)"] .${multiAirportFlairClass},
            body[style*="background-color: #000"] .${multiAirportFlairClass} {
                 background-color: #273440; /* Dim mode dark */
                 color: #e6c07b; /* Revert to standard flair text color in dark mode */
            }
            /* Consider a specific lights-out color if needed, for now dim is fine */

            .${multiAirportFlairClass} img.country-flag {
                width: 16px;
                height: 12px;
                margin-left: 4px;
                vertical-align: middle;
            }
            /* No specific hover title CSS needed */

            /* The dismiss button for multi-airport should also be covered by a combined selector or specific one */
            /* Combining dismiss button hover selectors for robustness */
            .${flairTag}:hover .dismiss-flair,
            .${multiAirportFlairClass}:hover .dismiss-flair {
                opacity: 0.85;
                pointer-events: auto;
            }
            .${flairTag} .dismiss-flair:hover,
            .${multiAirportFlairClass} .dismiss-flair:hover {
                opacity: 1;
                background-color: rgba(51, 51, 51, 0.85);
            }
        `;
    }

    function injectStyles() {
        // Remove existing styles if they exist to update dynamic ones (like dark mode)
        const existingStyleId = 'airport-flair-styles';
        let existingStyleElement = document.getElementById(existingStyleId);
        if (existingStyleElement) {
            existingStyleElement.remove();
        }
        GM_addStyle(getDynamicStyles());
        // Add an ID to the style element for easy removal/update if needed
        const styleElements = document.head.getElementsByTagName('style');
        if(styleElements.length > 0) {
             // Assume the last style element added by GM_addStyle is ours if we don't have a direct way to ID it from GM_addStyle
            styleElements[styleElements.length - 1].id = existingStyleId;
        }
    }

    // --- DOM Interaction & Manipulation ---
    function createFlairElement(code, airportInfo, originalCasing = null) {
        const anchor = document.createElement('a');
        anchor.href = `https://www.google.com/search?q=airport+${encodeURIComponent(code)}`;
        anchor.target = '_blank';
        anchor.rel = 'noopener noreferrer';
        anchor.classList.add(flairTag); // Standard flair tag
        anchor.classList.add(processedMark);

        let titleText = `${airportInfo.name} (${code})`;
        if (airportInfo.municipality) titleText += `, ${airportInfo.municipality}`;
        if (airportInfo.iso_country) titleText += `, ${airportInfo.iso_country}`;
        anchor.title = titleText;

        const codeTextNode = document.createTextNode(code);
        anchor.appendChild(codeTextNode); // Text first

        if (airportInfo.iso_country) {
            const flagImg = document.createElement('img');
            flagImg.src = `https://cdnjs.cloudflare.com/ajax/libs/flag-icon-css/4.1.5/flags/4x3/${airportInfo.iso_country.toLowerCase()}.svg`;
            flagImg.alt = `${airportInfo.iso_country} flag`;
            flagImg.classList.add('country-flag');
            anchor.appendChild(flagImg); // Flag after text
        }

        // Dismiss Button for single airport flairs
        const dismissBtn = document.createElement('span');
        dismissBtn.classList.add('dismiss-flair');
        dismissBtn.innerHTML = '&times;';
        dismissBtn.dataset.code = code;
        dismissBtn.dataset.type = 'single-airport'; // Explicitly mark type

        if (originalCasing && originalCasing !== code) {
            dismissBtn.dataset.originalCasing = originalCasing;
            dismissBtn.title = `Revert to '${originalCasing}' (potential code)`;
        } else {
            dismissBtn.title = `Revert to '${code}' (potential code)`;
        }

        dismissBtn.addEventListener('click', function(event) {
            console.log("[Airport Flair] Single Airport Dismiss button clicked:", event.target.dataset.code);
            event.preventDefault();
            event.stopPropagation();

            const currentCode = event.target.dataset.code;
            const originalMixedCasing = event.target.dataset.originalCasing;
            const currentFlairElement = event.target.closest('.' + flairTag);

            const codeForPotential = originalMixedCasing || currentCode;
            const airportInfoForReversion = airportData[codeForPotential.toUpperCase()];

            if (currentFlairElement && currentFlairElement.parentNode) {
                if (airportInfoForReversion) {
                    const potentialElement = createPotentialFlairElement(codeForPotential, airportInfoForReversion, false);
                    currentFlairElement.parentNode.replaceChild(potentialElement, currentFlairElement);
                } else {
                    const revertedTextSpan = document.createElement('span');
                    revertedTextSpan.textContent = codeForPotential;
                    revertedTextSpan.classList.add(processedMark);
                    currentFlairElement.parentNode.replaceChild(revertedTextSpan, currentFlairElement);
                }
            }
        });
        anchor.appendChild(dismissBtn);
        return anchor;
    }

    // Modified createPotentialFlairElement to handle both single and multi-airport types
    function createPotentialFlairElement(originalCode, info, isMultiAirport = false) {
        const span = document.createElement('span');
        span.classList.add(potentialFlairClass);
        span.classList.add(processedMark);
        span.textContent = originalCode;

        let titleText = `Recognize ${originalCode.toUpperCase()} as an `;
        if (isMultiAirport && info && info.name) {
            titleText += `area? (${info.name})`;
        } else if (!isMultiAirport && info && info.name) {
            titleText += `airport? (${info.name})`;
        } else {
            titleText += `code?`; // Fallback
        }
        span.title = titleText;

        span.dataset.originalCode = originalCode;
        span.dataset.isMulti = isMultiAirport ? "true" : "false";

        const clickListener = function(event) {
            event.preventDefault();
            event.stopPropagation();

            const codeToConvert = event.target.dataset.originalCode;
            const wasMulti = event.target.dataset.isMulti === "true";
            const uppercaseCode = codeToConvert.toUpperCase();

            let fullFlairElement;
            if (wasMulti) {
                const maInfo = multiAirportData[uppercaseCode];
                if (maInfo) {
                    fullFlairElement = createMultiAirportFlairElement(uppercaseCode, maInfo, codeToConvert);
                }
            } else {
                const airportInfo = airportData[uppercaseCode];
                if (airportInfo) {
                    fullFlairElement = createFlairElement(uppercaseCode, airportInfo, codeToConvert);
                }
            }

            if (fullFlairElement && event.target.parentNode) {
                event.target.parentNode.replaceChild(fullFlairElement, event.target);
            }
        };
        span.addEventListener('click', clickListener, { once: true });
        return span;
    }

    function replaceTextWithFlair(textNode) {
        if (!textNode.parentNode || textNode.parentNode.classList.contains(processedMark) ||
            textNode.parentNode.closest('a, script, style, input, textarea, [contenteditable="true"]')) {
            return;
        }

        const text = textNode.nodeValue;
        let match;
        let lastIndex = 0;
        const fragment = document.createDocumentFragment();
        let foundMatch = false;

        while ((match = iataRegex.exec(text)) !== null) {
            const originalCode = match[1];
            const uppercaseCode = originalCode.toUpperCase();

            const airportInfo = airportData[uppercaseCode];
            const maInfo = multiAirportData[uppercaseCode];

            if (COMMON_TLA_BLOCKLIST.has(uppercaseCode)) {
                if (airportInfo || maInfo) { // It's a common TLA but also a valid airport/MA code
                    foundMatch = true;
                    if (match.index > lastIndex) {
                        fragment.appendChild(document.createTextNode(text.substring(lastIndex, match.index)));
                    }
                    // Always create potential for blocklisted items that are also airports
                    fragment.appendChild(createPotentialFlairElement(originalCode, airportInfo || maInfo, !!maInfo));
                    lastIndex = iataRegex.lastIndex;
                }
                // If it's in the blocklist and NOT an airport, we do nothing, effectively skipping it.
                // The loop will continue, and this part of the text remains unchanged.
            } else if (airportInfo || maInfo) { // Not in blocklist, but is an airport/MA code
                foundMatch = true;
                if (match.index > lastIndex) {
                    fragment.appendChild(document.createTextNode(text.substring(lastIndex, match.index)));
                }

                if (originalCode === uppercaseCode) { // Already all caps
                    if (airportInfo) {
                        fragment.appendChild(createFlairElement(uppercaseCode, airportInfo));
                    } else { // maInfo must be true
                        fragment.appendChild(createMultiAirportFlairElement(uppercaseCode, maInfo));
                    }
                } else { // Mixed or lowercase - create potential
                    if (airportInfo) {
                        fragment.appendChild(createPotentialFlairElement(originalCode, airportInfo, false));
                    } else { // maInfo must be true
                        fragment.appendChild(createPotentialFlairElement(originalCode, maInfo, true));
                    }
                }
                lastIndex = iataRegex.lastIndex;
            }
            // If no conditions met (not blocklisted, not airport), loop continues, text remains.
        }

        if (foundMatch) {
            if (lastIndex < text.length) {
                fragment.appendChild(document.createTextNode(text.substring(lastIndex)));
            }
            textNode.parentNode.replaceChild(fragment, textNode);
        }
    }

    function processNode(node) {
        if (node.nodeType === Node.TEXT_NODE) {
            replaceTextWithFlair(node);
        }
        else if (node.nodeType === Node.ELEMENT_NODE &&
                 !node.classList.contains(processedMark) &&
                 !node.closest('a, script, style, input, textarea, [contenteditable="true"], .option-text, .' + flairTag + ', .' + potentialFlairClass + ', .' + multiAirportFlairClass)) {
             Array.from(node.childNodes).forEach(child => processNode(child));
        }
    }

    function processAddedNodes(mutationList) {
        for (const mutation of mutationList) {
            if (mutation.type === 'childList') {
                mutation.addedNodes.forEach(node => {
                    // Check if the node itself has already been processed (e.g. if it's part of a flair we just added)
                    if (node.nodeType === Node.ELEMENT_NODE && node.classList.contains(processedMark)) {
                        return;
                    }
                    processNode(node);
                });
            }
        }
        // After processing mutations, it's good to re-evaluate dynamic styles like dark mode
        injectStyles();
    }

    function observeDOMChanges() {
        if (observer) observer.disconnect(); // Disconnect previous observer if any

        observer = new MutationObserver(processAddedNodes);
        observer.observe(document.body, {
            childList: true,
            subtree: true
        });
        console.log("MutationObserver started.");
    }

    function processPage() {
        console.log("Processing page for airport codes...");
        injectStyles();
        // Initial scan of the entire body
        processNode(document.body);
        observeDOMChanges();
    }

    // --- Cleanup ---
    window.addEventListener('unload', () => {
        if (observer) {
            observer.disconnect();
            console.log("Disconnected MutationObserver.");
        }
    });

    // --- Create Flair Elements ---
    // Function to create the actual styled flair element for MULTI-AIRPORT codes
    function createMultiAirportFlairElement(code, maInfo, originalCasing = null) {
        const anchor = document.createElement('a');
        anchor.href = `https://www.google.com/search?q=${encodeURIComponent(maInfo.name)}`; // Search for the area name
        anchor.target = '_blank';
        anchor.rel = 'noopener noreferrer';
        anchor.classList.add(multiAirportFlairClass); // Use special class
        anchor.classList.add(processedMark);

        anchor.title = `${maInfo.name} (${code})`;

        // Flag logic: use primaryAirportForFlag from maInfo to look up in airportData
        if (maInfo.primaryAirportForFlag && airportData[maInfo.primaryAirportForFlag]) {
            const primaryAirportDetails = airportData[maInfo.primaryAirportForFlag];
            if (primaryAirportDetails.iso_country) {
                const flagImg = document.createElement('img');
                flagImg.src = `https://cdnjs.cloudflare.com/ajax/libs/flag-icon-css/4.1.5/flags/4x3/${primaryAirportDetails.iso_country.toLowerCase()}.svg`;
                flagImg.alt = `${primaryAirportDetails.iso_country} flag`;
                flagImg.classList.add('country-flag');
                anchor.appendChild(flagImg); // Flag first for multi-airport to differentiate?
            }
        }

        const codeTextNode = document.createTextNode(code);
        if (anchor.firstChild && anchor.firstChild.tagName === 'IMG') { // If flag was added, append text after
            anchor.appendChild(codeTextNode);
        } else { // Otherwise, text is first
            anchor.insertBefore(codeTextNode, anchor.firstChild);
        }

        // Add Dismiss Button
        const dismissBtn = document.createElement('span');
        dismissBtn.classList.add('dismiss-flair');
        dismissBtn.innerHTML = '&times;';
        dismissBtn.dataset.code = code; // Uppercase code of the flair
        dismissBtn.dataset.type = 'multi-airport'; // Mark type for potential differentiated dismiss logic

        // Determine title and if originalCasing needs to be stored for dismiss
        if (originalCasing && originalCasing !== code) {
            dismissBtn.dataset.originalCasing = originalCasing;
            dismissBtn.title = `Revert to '${originalCasing}' (potential code)`;
        } else {
            dismissBtn.title = `Revert to '${code}' (potential code)`;
        }

        dismissBtn.addEventListener('click', function(event) {
            console.log("[Airport Flair] Multi-Airport Dismiss button clicked:", event.target.dataset.code);
            event.preventDefault();
            event.stopPropagation();

            const currentCode = event.target.dataset.code;
            const originalMixedCasing = event.target.dataset.originalCasing;
            const currentFlairElement = event.target.closest('.' + multiAirportFlairClass);
            const codeForPotential = originalMixedCasing || currentCode;
            const maInfoForReversion = multiAirportData[codeForPotential.toUpperCase()];
            // Also check regular airport data in case it's an ambiguous code that got here
            const airportInfoForReversion = airportData[codeForPotential.toUpperCase()];

            if (currentFlairElement && currentFlairElement.parentNode) {
                const infoForPotential = maInfoForReversion || airportInfoForReversion; // Prefer MA info if available
                if (infoForPotential) {
                    const potentialElement = createPotentialFlairElement(codeForPotential, infoForPotential, !!maInfoForReversion);
                    currentFlairElement.parentNode.replaceChild(potentialElement, currentFlairElement);
                } else {
                    // Fallback to plain text if no info found (should be rare)
                    const revertedTextSpan = document.createElement('span');
                    revertedTextSpan.textContent = codeForPotential;
                    revertedTextSpan.classList.add(processedMark);
                    currentFlairElement.parentNode.replaceChild(revertedTextSpan, currentFlairElement);
                }
            }
        });
        anchor.appendChild(dismissBtn);
        return anchor;
    }

})();