Holotower Inline Quoting

Inline Quoting for holotower.org

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         Holotower Inline Quoting
// @namespace    http://tampermonkey.net/
// @version      4.3
// @author       grem
// @license      MIT
// @description  Inline Quoting for holotower.org
// @match        *://boards.holotower.org/*
// @match        *://holotower.org/*
// @grant        GM_addStyle
// @grant        GM_xmlhttpRequest
// @require      https://code.jquery.com/jquery-3.6.0.min.js
// @connect      self
// @connect      boards.holotower.org
// @icon         data:image/gif;base64,R0lGODlhIAAgAHAAACH5BAEAAPsALAAAAAAgACAAh2x+yAc0uCFFvQo5vgI5wwY7wjpexwo8wQQ8xAU/xgVAyAxEyVh60w0/wwZAyAhCzAhEzghGzwhH0hRNz3eV3YnB7kin7E+v8VCz8VC571G88VHC8UzF9HrP7g1EyAdDzQlI0QpJ1AtL1gpO1w1P2ihf2Wiy8Rub+R6i+R6p+iKv+iO2/CK8/SvB+g9GzAhH0QpJ0wxN1wtP2Q1R2w5S3w1V3xFX4UJ142u28B6f+SKl9yKs+ySz+yS6/CXA/XHS9w1KzwpK1QtN1QtP1w1Q2w1S3Q9U3xBX4BBY5BBb5Q5d6BVf5laI5mu68R+i+CKq+iOw+yS3/CS9/jPC+XKO2nST3nSU3nOV4HSV4nWX5XaY43Sb5nib5nad6Hed6nWd7W6b64Ck6mu88R+m+iOt+iS0+yS7/SfB/ZDb9my88iCr+iSx+yS4/CS//kfH92y/8CKu+SS1+yS8/SrB/GzA8yGz+iS5/STA/mHN92rD8yO2+yS9/S7C+o6s3mzF8yO6/CbA/X/V9pGy7l2O5m3G8yK+/TvE+CNz7GOX6mzJ8yjA/J/f9nap7hR18mie7GbK9EvH9iB+7xV99Wmi7mnK8Fif8BmE8xiE9Wqn8CGL8huL9xqL9myr8EGe7xuT9xyT9xqT92yx8Y3H8iGa9h+b+B6b+B2b+Gy38S2k9B+i+SCi+G67827C8CCp+iGp+h+p+m/A83bT82nR93HQ9nHM9nDK9m3J9W7G9W/E83DA9G6/82y982258mu183u58Cqw9yOx+iSw+yKw+3LF9FnK9CvB/CO//SS7/CS3+yO0+iOw+SGs+iCo+iGk+B+h+B6d+iub71PC9CO4/SS5/CW4/CO4/HHL9UjH9ijB/SO+/iS6/SS2/CSy+yOu+yKr+iKn+B+j+TKm9Jzd9Si/+yPA/m3N9pbc9jfD+SOt+yCq+zSr9ZPX8IHW94LX9n7V9nnU9SS0/COw+jey81/N9ji29kjH9zm+9pTc9jPC+DTB94jW8QAAAAAAAAAAAAAAAAAAAAj/APcJFAhgoMGDCBMiDCBAocOHAwcQKGAAosWDBxAkULCAwcWLDRw8gBBBwgQKDytYuIAhg4YNHDoc9PAhAogQIkaQKOHQxAkUKVSsYNECoYsXMETEkDGDRg0bNxLiyKFjB48ePn4gBBJEyBAiRYwcQZJEyRImB5s4eQIlipQpCalUsXIFSxYtW7h08fIFTBiDYsaQKWPmDJqPENOoWcOmjRvEEN/AiSNnDmSIdOrYuYPn8sM8evbwudjno58/gAJZFDToI6FChi4eQvQxkaJFFhk1cvTxEaSLkSRN+kjpYiVLlzB5fphJ0yZOyxV28vQJVKjoCEWNIlXK1CnsB1GlnFKlCsUq8ANZtXLF/hUsg7FkzaJVy9YtXLl07eLVy1fCX8AEI6AwwxxETDHGHINMMsosw0wzzjwDDULRSDMNNRhWY01C12CTjTbbcNONN9+AE85B4ozjw4orklOOQuac48Me08SxBjrpqHPQOuy046OP7kD0Dh93yEENPPHIg95A88zRhhlR0LOkQfWckY09Uxp0Dz75ZHmQPgcFBAA7
// @run-at       document-start
// ==/UserScript==

/* global $, setting */

const IQ_SETTINGS_KEY = "InlineQuote Settings";
const iqDefaultSettings = {
    enableVideoHoverPreview: false,
    hideInlinedPosts: true,
    alwaysInline: true,
};

let iqSettings = {};
try {
    iqSettings = JSON.parse(localStorage.getItem(IQ_SETTINGS_KEY)) || {};
} catch(e) { iqSettings = {}; }
for (const k in iqDefaultSettings) if (!(k in iqSettings)) iqSettings[k] = iqDefaultSettings[k];

function saveIQ() {
    localStorage.setItem(IQ_SETTINGS_KEY, JSON.stringify(iqSettings));
}

function getGlobalDefaultVolume() {
    try {
        if (typeof setting === "function") {
            const v = parseFloat(setting("videovolume"));
            if (!isNaN(v)) return Math.min(Math.max(v,0),1);
        }
    } catch {}
    return 1;
}

function cleanInlineContainer($container) {
    $container.find('> .inline-cloned-post > p.intro').each(function() {
        let next = this.nextSibling;
        while (next && next.nodeType === 3 && !/\S/.test(next.nodeValue)) {
            let toRemove = next;
            next = next.nextSibling;
            toRemove.parentNode.removeChild(toRemove);
        }
    });

    $container.find('> .inline-cloned-post > .files').each(function() {
        let next = this.nextSibling;
        while (next && next.nodeType === 3 && !/\S/.test(next.nodeValue)) {
            let toRemove = next;
            next = next.nextSibling;
            toRemove.parentNode.removeChild(toRemove);
        }

        if (!this.innerText.trim() && !this.querySelector('img, video, a, object, embed')) {
            this.parentNode.removeChild(this);
        }
    });
}

$(function () {
    if (typeof Options === "undefined") return;
    const tab = Options.add_tab("inline-quote", "quote-right", "Inline Quotes");
    const $content = $("<div></div>");
    const $videoHoverEntry = $(
        `<div id="enableVideoHoverPreview-container">
            <label style="text-decoration: underline; cursor: pointer;">
                <input type="checkbox" id="enableVideoHoverPreview">Play videos on hover</label>
            <span class="description">: Show/Hide previews when hovering videos inside inline quotes</span>
        </div>`);
    const $hideInlinedEntry = $(
        `<div id="hideInlinedPosts-container">
            <label style="text-decoration: underline; cursor: pointer;">
                <input type="checkbox" id="hideInlinedPosts">Hide inlined posts</label>
            <span class="description">: Hide original posts when they are inlined</span>
        </div>`);
    const $alwaysInlineEntry = $(
        `<div id="alwaysInline-container">
            <label style="text-decoration: underline; cursor: pointer;">
                <input type="checkbox" id="alwaysInline">Always inline posts</label>
            <span class="description">: Inline posts even if they are already visible on screen</span>
        </div>`);
    $content.append($videoHoverEntry).append($hideInlinedEntry).append($alwaysInlineEntry);
    $(tab.content).append($content);
    $("#enableVideoHoverPreview").prop("checked", iqSettings.enableVideoHoverPreview);
    $("#enableVideoHoverPreview").on("change", function () {
        iqSettings.enableVideoHoverPreview = this.checked;
        saveIQ();
    });
    $("#hideInlinedPosts").prop("checked", iqSettings.hideInlinedPosts);
    $("#hideInlinedPosts").on("change", function () {
        iqSettings.hideInlinedPosts = this.checked;
        saveIQ();
    });
    $("#alwaysInline").prop("checked", iqSettings.alwaysInline);
    $("#alwaysInline").on("change", function () {
        iqSettings.alwaysInline = this.checked;
        saveIQ();
    });
});

(function() {
    'use strict';

    const INLINE_CONTAINER_CLASS = 'inline-quote-container';
    const INLINE_ACTIVE_LINK_CLASS = 'inline-active';
    const LOADING_DATA_ATTR = 'data-inline-loading';
    const ERROR_DATA_ATTR = 'data-inline-error';
    const TEMP_HIGHLIGHT_CLASS = 'inline-temp-highlight';
    const PROCESSED_ATTR = 'data-inline-processed';
    const INLINED_ID_ATTR = 'data-inlined-id';
    const CLONED_POST_CLASS = 'inline-cloned-post';
    const CLONED_HOVER_PREVIEW_ID_PREFIX = 'post-hove-';
    const HOVER_INITIALIZED_ATTR = 'data-iq-hover-init';
    const SITE_PREVIEW_BASE_CLASSES = 'thread post-hover reply post';
    const SITE_PREVIEW_REPLY_CLASS = 'reply';
    const SITE_PREVIEW_OP_CLASS = 'op';
    const INLINE_HIDDEN_CLASS = 'iq-hidden-post';

    const POST_SELECTOR_ID_FORMAT = (postId) => `div.post[id$='_${postId}']`;
    const POTENTIAL_QUOTE_LINK_SELECTOR = "a[onclick*='highlightReply'], a[href*='#q']";
    const QUOTE_LINK_REGEX = /^>>(?:>\/[a-zA-Z0-9_-]+\/)?(\d+)/;
    const SITE_HOVER_TARGET_SELECTOR = 'div.body a:not([rel="nofollow"]), span.mentioned a:not([rel="nofollow"]), p.intro a.post_no:not([rel="nofollow"])';
    const BOARD_CONTEXT_SELECTOR = '[data-board]';

    GM_addStyle(`
        .${INLINE_CONTAINER_CLASS} { display: table; border: 1px dashed var(--subtle-border-color,#888); background-color: var(--inline-background-color,rgba(128,128,128,.05)); padding:5px; margin-top:5px; margin-left:15px; border-radius:4px; max-width:100%; }
        .${INLINE_CONTAINER_CLASS} > .${CLONED_POST_CLASS}[data-board] { display:block!important; border:none!important; margin:0!important; padding:0!important; box-shadow:none!important; background:transparent!important; width:100%!important; }
        section.post { display: inline-block; background: inherit; border-color: inherit; border-width: inherit; border-style: inherit; }
        section.post.reply { background: var(--reply-background-color, #d6daf0); border: 1px solid var(--subtle-border-color, #b7c5d9); border-left: none; border-top: none; }
        a.${INLINE_ACTIVE_LINK_CLASS} { font-weight:bold!important; color:var(--link-hover-color,#d11a1a)!important; opacity:.85; text-decoration:underline dotted!important; }
        a.${INLINE_ACTIVE_LINK_CLASS}:hover { opacity:1 }
        a[${LOADING_DATA_ATTR}="true"]::after { content:" (loading.)"; font-style:italic; color:var(--text-color-muted,#888); margin-left:4px }
        a[${ERROR_DATA_ATTR}="true"]::after   { content:" (not found)"; font-style:italic; color:var(--error-text-color,#f00); margin-left:4px }
        .${TEMP_HIGHLIGHT_CLASS} { transition:outline .1s ease-in-out; outline:2px solid var(--highlight-color,yellow)!important; outline-offset:2px }
        div.post-hover[id^="${CLONED_HOVER_PREVIEW_ID_PREFIX}"] { position:absolute!important; z-index:150!important; max-width:500px }
        div.post-hover[id^="${CLONED_HOVER_PREVIEW_ID_PREFIX}"] > .post { border:none!important; margin:0!important; padding:0!important; box-shadow:none!important; background:transparent!important; }
        div.post-hover[id^="${CLONED_HOVER_PREVIEW_ID_PREFIX}"] .hide-post-button,div.post-hover[id^="${CLONED_HOVER_PREVIEW_ID_PREFIX}"] .menu-button,div.post-hover[id^="${CLONED_HOVER_PREVIEW_ID_PREFIX}"] input[type=checkbox].delete { display:none!important }
        div.post-hover[id^="${CLONED_HOVER_PREVIEW_ID_PREFIX}"].loading-preview::after { content:"Loading."; font-style:italic; color:var(--text-color-muted,#888); padding:5px; display:block }
        div.post-hover[id^="${CLONED_HOVER_PREVIEW_ID_PREFIX}"].error-preview::after { content:"Not found."; font-style:italic; color:var(--error-text-color,#f00); padding:5px; display:block }
        .${INLINE_HIDDEN_CLASS} { display: none !important; }
    `);

    function getPostIdFromLink(link) {
        if (!link) return null;
        const textMatch = link.textContent?.trim().match(QUOTE_LINK_REGEX);
        if (textMatch) return textMatch[1];
        const hrefMatch = link.getAttribute('href')?.match(/#(\d+)$/);
        if (hrefMatch) return hrefMatch[1];
        const quoteHrefMatch = link.getAttribute('href')?.match(/#q(\d+)$/);
        if (quoteHrefMatch) return quoteHrefMatch[1];
        return null;
    }

    function fetchPostHtml(url) {
        const fetchUrl = url?.split('#')[0];
        if (!fetchUrl) return Promise.resolve(null);
        return fetch(fetchUrl)
            .then(r => r.ok ? r.text() : null)
            .catch(() => null);
    }

    function parseAndFindPost(html, postId) {
        try {
            const parser = new DOMParser();
            const doc = parser.parseFromString(html, 'text/html');
            const postElement = doc.querySelector(POST_SELECTOR_ID_FORMAT(postId));
            if (postElement && postElement.classList.contains('op')) {
                const filesElement = postElement.previousElementSibling;
                if (filesElement && filesElement.classList.contains('files')) {
                    const wrapper = document.createElement('div');
                    wrapper.appendChild(filesElement.cloneNode(true));
                    wrapper.appendChild(postElement.cloneNode(true));
                    return wrapper;
                }
            }
            return postElement;
        } catch (error) {
            return null;
        }
    }

    function isElementInViewportStrict(el) {
        el = el && el[0] ? el[0] : el;
        if (!el) return false;
        const rect = el.getBoundingClientRect();
        return rect.top < window.innerHeight && rect.bottom > 0 && rect.left < window.innerWidth && rect.right > 0;
    }

    function temporaryHighlight(el) {
        el = el && el[0] ? el[0] : el;
        if (!el) return;
        $(el).addClass(TEMP_HIGHLIGHT_CLASS);
        setTimeout(() => { $(el).removeClass(TEMP_HIGHLIGHT_CLASS); }, 500);
    }

    function processLinks(parentElement) {
        if (!parentElement) return;
        const links = parentElement.querySelectorAll(POTENTIAL_QUOTE_LINK_SELECTOR);
        links.forEach(link => {
            if (!link.hasAttribute(PROCESSED_ATTR)) {
                const onclickValue = link.getAttribute('onclick');
                if (onclickValue && onclickValue.includes('highlightReply')) {
                    link.removeAttribute('onclick');
                }
                link.setAttribute(PROCESSED_ATTR, 'true');
            }
        });
    }

    function toggleOriginalVisibility(postId, hide) {
        const original = document.querySelector(POST_SELECTOR_ID_FORMAT(postId));
        if (!original) return;

        let startNode = original;
        if (original.classList.contains('op') && original.previousElementSibling?.classList.contains('files')) {
            startNode = original.previousElementSibling;
        }

        const elementsToToggle = [original];
        if (startNode !== original) {
            elementsToToggle.push(startNode);
        }

        let prev = startNode.previousSibling;
        while (prev) {
            if (prev.nodeType === Node.ELEMENT_NODE) {
                if (
                    prev.classList.contains('post') ||
                    prev.classList.contains('files') ||
                    prev.classList.contains('inline-quote-container') ||
                    prev.tagName === 'HR' ||
                    prev.tagName === 'FORM'
                ) {
                    break;
                }
            }
            elementsToToggle.push(prev);
            prev = prev.previousSibling;
        }

        elementsToToggle.forEach(el => {
            if (el.nodeType === Node.ELEMENT_NODE) {
                if (hide) {
                    el.classList.add(INLINE_HIDDEN_CLASS);
                } else {
                    el.classList.remove(INLINE_HIDDEN_CLASS);
                }
            }
        });
    }

    async function handleInlineQuoteClick(linkElement, postId) {
        const $link = $(linkElement);
        let $mainPost = $link.closest('.post.reply, .post.op, section.post');
        if ($mainPost.hasClass('post-hover')) {
            const match = $mainPost.attr('id') && $mainPost.attr('id').match(/^post-hover-(\d+)/);
            if (match) {
                $mainPost = $(`.post.reply#reply_${match[1]}, .post.op#op_${match[1]}`);
            }
        }
        const $body = $mainPost.find('.body').first();

        const $existingContainer = $mainPost.find(`.${INLINE_CONTAINER_CLASS}[${INLINED_ID_ATTR}="${postId}"]`);
        if ($existingContainer.length) {
            const nestedIds = [];
            if (iqSettings.hideInlinedPosts) {
                $existingContainer.find(`.${INLINE_CONTAINER_CLASS}`).each(function() {
                    const nId = $(this).attr(INLINED_ID_ATTR);
                    if (nId) nestedIds.push(nId);
                });
            }

            $existingContainer.remove();
            $('#chx_hoverImage').remove();
            $('.iq-media-hover-preview').remove();

            if (iqSettings.hideInlinedPosts) {
                nestedIds.forEach(nId => {
                    if ($(`.${INLINE_CONTAINER_CLASS}[${INLINED_ID_ATTR}="${nId}"]`).length === 0) {
                        toggleOriginalVisibility(nId, false);
                    }
                });
            }
        }

        if ($link.hasClass(INLINE_ACTIVE_LINK_CLASS)) {
            $link.removeClass(INLINE_ACTIVE_LINK_CLASS).removeAttr(LOADING_DATA_ATTR).removeAttr(ERROR_DATA_ATTR);
            if (iqSettings.hideInlinedPosts) {
                if ($(`.${INLINE_CONTAINER_CLASS}[${INLINED_ID_ATTR}="${postId}"]`).length === 0) {
                    toggleOriginalVisibility(postId, false);
                }
            }
            return;
        }

        let targetPostElement = document.querySelector(POST_SELECTOR_ID_FORMAT(postId));
        let boardValue = null;
        const $linkContext = $link.closest(BOARD_CONTEXT_SELECTOR);
        if ($linkContext.length > 0) boardValue = $linkContext.data('board');
        else if (targetPostElement) {
            const $targetContext = $(targetPostElement).closest(BOARD_CONTEXT_SELECTOR);
            if ($targetContext.length > 0) boardValue = $targetContext.data('board');
        }

        if (!iqSettings.alwaysInline && targetPostElement && isElementInViewportStrict(targetPostElement)) {
            temporaryHighlight(targetPostElement);
            $link.removeClass(INLINE_ACTIVE_LINK_CLASS).removeAttr(LOADING_DATA_ATTR).removeAttr(ERROR_DATA_ATTR);
            return;
        }

        let isAncestor = false;
        let $highlightTarget = null;

        $link.parents('.post').each(function() {
            let pid = null;
            if ($(this).hasClass('inline-cloned-post')) {
                pid = $(this).closest(`.${INLINE_CONTAINER_CLASS}`).attr(INLINED_ID_ATTR);
            } else {
                const match = $(this).attr('id')?.match(/^(?:reply_|op_)(\d+)/);
                if (match) pid = match[1];
            }
            if (pid === postId) {
                isAncestor = true;
                $highlightTarget = $(this);
                return false;
            }
        });

        if (isAncestor) {
            temporaryHighlight($highlightTarget);
            $link.removeClass(INLINE_ACTIVE_LINK_CLASS).removeAttr(LOADING_DATA_ATTR).removeAttr(ERROR_DATA_ATTR);
            return;
        }

        $link.addClass(INLINE_ACTIVE_LINK_CLASS).attr(LOADING_DATA_ATTR, "true");

        if (!targetPostElement && linkElement.href) {
            const postHtml = await fetchPostHtml(linkElement.href);
            if (postHtml) {
                const parsed = parseAndFindPost(postHtml, postId);
                if (parsed) targetPostElement = parsed;
            }
        }
        $link.removeAttr(LOADING_DATA_ATTR);

        if (targetPostElement) {
            let sourceElementToClone = targetPostElement;
            if (sourceElementToClone.classList && sourceElementToClone.classList.contains('op') && sourceElementToClone.parentNode) {
                const filesElement = sourceElementToClone.previousElementSibling;
                if (filesElement && filesElement.classList.contains('files')) {
                    const wrapper = document.createElement('div');
                    wrapper.appendChild(filesElement.cloneNode(true));
                    wrapper.appendChild(sourceElementToClone.cloneNode(true));
                    sourceElementToClone = wrapper;
                }
            }

            const $container = $('<div>')
                .addClass(INLINE_CONTAINER_CLASS)
                .attr(INLINED_ID_ATTR, postId);

            const $mentionedSpan = $link.closest('span.mentioned.unimportant');

            let doInsert = () => {};

            if ($mentionedSpan.length) {
                let $insertionAnchor = $mentionedSpan;
                const $linksInSpan = $mentionedSpan.find('a');
                const clickedLinkIndex = $linksInSpan.index($link);

                for (let i = clickedLinkIndex - 1; i >= 0; i--) {
                    const id = getPostIdFromLink($linksInSpan[i]);
                    if (id) {
                        const $containerForLink = $mainPost.find(`.${INLINE_CONTAINER_CLASS}[${INLINED_ID_ATTR}="${id}"]`);
                        if ($containerForLink.length) {
                            $insertionAnchor = $containerForLink.first();
                            break;
                        }
                    }
                }
                doInsert = () => $insertionAnchor.after($container);
            } else {
                let insertAfterTarget = $link[0];

                while (true) {
                    let nextNode = insertAfterTarget.nextSibling;
                    if (!nextNode) break;

                    if (nextNode.nodeType === 3 && /^\s*$/.test(nextNode.nodeValue)) {
                        let peek = nextNode.nextSibling;
                        if (peek && peek.nodeType === 1 && peek.tagName === 'SMALL') {
                            insertAfterTarget = peek;
                            continue;
                        } else {
                            break;
                        }
                    } else if (nextNode.nodeType === 1 && nextNode.tagName === 'SMALL') {
                        insertAfterTarget = nextNode;
                        continue;
                    }
                    break;
                }

                doInsert = () => $(insertAfterTarget).after($container);
            }

            let handled = false;
            if (window.g?.posts) {
                const boardID = boardValue;
                const postKey = `${boardID}.${postId}`;
                const postObj = g.posts.get(postKey);
                if (postObj && typeof postObj.addClone === 'function') {
                    const cloneObj = postObj.addClone($container[0], false);
                    $(cloneObj.nodes.root)
                        .addClass(CLONED_POST_CLASS)
                        .attr(PROCESSED_ATTR, 'true');
                    handled = true;
                }
            }
            if (!handled) {
                const cloned = document.createElement('section');
                let actualPost = sourceElementToClone;
                if (sourceElementToClone && !sourceElementToClone.classList.contains('post')) {
                    actualPost = sourceElementToClone.querySelector('.post');
                }

                let originalClasses = actualPost ? actualPost.className : '';
                originalClasses = originalClasses.replace(new RegExp('\\b' + INLINE_HIDDEN_CLASS + '\\b', 'g'), '').replace(/\s+/g, ' ').trim();
                cloned.className = originalClasses + ' ' + CLONED_POST_CLASS;

                const postContent = sourceElementToClone.cloneNode(true);

                if (postContent.removeAttribute) postContent.removeAttribute('id');
                postContent.querySelectorAll('[id]').forEach(el => el.removeAttribute('id'));

                $(postContent).find('a[data-expanded="true"]').each(function() {
                    $(this).removeData('expanded').removeAttr('data-expanded');
                    $(this).find('img.full-image').remove();
                    $(this).find('img.post-image').css({opacity: '', display: ''});
                });

                while (postContent.firstChild) {
                    cloned.appendChild(postContent.firstChild);
                }

                cloned.setAttribute('id', 'thread_0');
                if (boardValue) cloned.setAttribute('data-board', boardValue);
                else cloned.setAttribute('data-board-missing', 'true');

                $(cloned).find('.' + INLINE_HIDDEN_CLASS).removeClass(INLINE_HIDDEN_CLASS);

                $container.append(cloned);

                $container.on('click', 'a', function(e) {
                    const thumb = this.childNodes[0];
                    if (thumb && thumb.className && thumb.className.match(/post-image/) && !this.className.match(/file/)) {
                        if (e.which === 2 || e.ctrlKey || e.shiftKey || e.metaKey) return;

                        e.stopPropagation();

                        $('#chx_hoverImage, .iq-media-hover-preview').remove();

                        const $a = $(this);
                        const $thumb = $(thumb);

                        if (!this.onclick) {
                            e.preventDefault();
                            if (!$a.data('expanded')) {
                                $a.data('expanded', 'true');
                                $thumb.css({'opacity': '0.4', 'pointer-events': 'none'});
                                const $full = $('<img>').addClass('full-image').css('display', 'none').attr('alt', 'Fullsized image');
                                $full.on('load', function() {
                                    $thumb.css('display', 'none');
                                    $full.css('display', '');
                                });
                                $full.attr('src', $a.attr('href'));
                                $a.append($full);
                            } else {
                                $a.removeData('expanded');
                                $a.find('.full-image').remove();
                                $thumb.css({'opacity': '', 'display': '', 'pointer-events': ''});
                            }
                        } else {
                            setTimeout(() => {
                                if ($a.data('expanded')) {
                                    $thumb.css('pointer-events', 'none');
                                    $('#chx_hoverImage').remove();
                                } else {
                                    $thumb.css('pointer-events', '');
                                    $('#chx_hoverImage').remove();
                                }
                            }, 10);
                        }
                    }
                });

                cleanInlineContainer($container);

                doInsert();

                initializeInlineHover(cloned);
                initializeInlineImageHover(cloned);

                $(document).trigger('new_post', [cloned]);
            } else {
                doInsert();
            }

            if (!$mentionedSpan.length) {
                $container.find('.inline-cloned-post')[0]?.style.setProperty('max-width', '100%', 'important');
                $container.find('.inline-cloned-post').css('box-sizing','border-box');
            }

            if (iqSettings.hideInlinedPosts) {
                toggleOriginalVisibility(postId, true);
            }

        } else {
            $link.attr(ERROR_DATA_ATTR, "true").removeClass(INLINE_ACTIVE_LINK_CLASS);
            setTimeout(() => { $link.removeAttr(ERROR_DATA_ATTR); }, 3000);
        }
    }

    function initializeInlineHover(parentElement) {
        let $preview = null;
        let targetPostId = null;
        let currentBoard = $(parentElement).closest(BOARD_CONTEXT_SELECTOR).data('board') || null;
        let fetchController = null;
        let mouseMoveTimer = null;

        function updatePreviewPosition(e) {
            if (!$preview) return;
            let top = e.pageY + 10; let left = e.pageX + 10;
            const win = $(window); const winHeight = win.height(); const winWidth = win.width();
            const previewElement = $preview[0];
            const previewHeight = previewElement.offsetHeight; const previewWidth = previewElement.offsetWidth;
            const scrollTop = win.scrollTop(); const scrollLeft = win.scrollLeft();
            if (previewHeight > 0 && top + previewHeight > scrollTop + winHeight) top = e.pageY - previewHeight - 10;
            if (top < scrollTop) top = scrollTop + 5;
            if (previewWidth > 0 && left + previewWidth > scrollLeft + winWidth) left = e.pageX - previewWidth - 10;
            if (left < scrollLeft) left = scrollLeft + 5;
            previewElement.style.top = top + 'px';
            previewElement.style.left = left + 'px';
        }

        function preparePreviewContent(sourceElement) {
            const clonedContent = sourceElement.cloneNode(true);
            clonedContent.removeAttribute('id'); clonedContent.querySelectorAll('[id]').forEach(el => el.removeAttribute('id'));
            return clonedContent;
        }

        function createPreviewDiv(sourceElement, postId) {
            $(`div[id^="${CLONED_HOVER_PREVIEW_ID_PREFIX}"]`).remove();
            const $previewContainer = $('<div>')
                .addClass(SITE_PREVIEW_BASE_CLASSES)
                .attr('id', CLONED_HOVER_PREVIEW_ID_PREFIX + postId)
                .css({
                    'border-style': 'solid',
                    'box-shadow': 'rgb(153, 153, 153) 1px 1px 1px',
                    'display': 'block',
                    'position': 'absolute',
                    'font-style': 'normal',
                    'z-index': '100'
                })
                .appendTo('body');
            if (currentBoard) $previewContainer.attr('data-board', currentBoard);
            if (sourceElement) {
                if (sourceElement.classList.contains(SITE_PREVIEW_REPLY_CLASS)) $previewContainer.addClass(SITE_PREVIEW_REPLY_CLASS);
                else if (sourceElement.classList.contains(SITE_PREVIEW_OP_CLASS)) $previewContainer.addClass(SITE_PREVIEW_OP_CLASS);
                $previewContainer.append(preparePreviewContent(sourceElement));
            }
            return $previewContainer;
        }

        $(parentElement).on('mouseenter.iqhover', SITE_HOVER_TARGET_SELECTOR, function(e) {
            const $link = $(this);
            if ($link.hasClass(INLINE_ACTIVE_LINK_CLASS)) return;

            const $parentPost = $link.closest('.post.reply[id^="reply_"]');
            const parentPostId = $parentPost.length ? $parentPost[0].id.replace(/^reply_/, '') : null;
            const postId = getPostIdFromLink(this);

            if (parentPostId && postId && parentPostId === postId) {
                return;
            }

            targetPostId = postId;
            const $parentInline = $link.parents(`.${INLINE_CONTAINER_CLASS}[${INLINED_ID_ATTR}]`);
            if ($parentInline.filter(`[${INLINED_ID_ATTR}="${targetPostId}"]`).length) {
                return;
            }
            if (!targetPostId || !currentBoard) return;
            $(`div[id^="${CLONED_HOVER_PREVIEW_ID_PREFIX}"]`).remove();
            const originalPostElement = document.querySelector(POST_SELECTOR_ID_FORMAT(targetPostId));
            if (originalPostElement && $(originalPostElement).closest(BOARD_CONTEXT_SELECTOR).data('board') === currentBoard) {
                $preview = createPreviewDiv(originalPostElement, targetPostId);
                updatePreviewPosition(e);
            } else {
                const url = $link.attr('href'); if (!url) return;
                $preview = createPreviewDiv(null, targetPostId);
                $preview.addClass('loading-preview');
                updatePreviewPosition(e);
                if (fetchController) fetchController.abort();
                const controller = new AbortController(); fetchController = controller;
                const fetchUrl = url.split('#')[0];

                fetch(fetchUrl, { signal: controller.signal })
                    .then(r => r.ok ? r.text() : null)
                    .then(html => {
                        if (controller.signal.aborted) return;
                        fetchController = null;
                        if (html) {
                            const fetchedPostElement = parseAndFindPost(html, targetPostId);
                            if (fetchedPostElement) {
                                if ($preview) {
                                    $preview.empty().removeClass('loading-preview loading-error').addClass(SITE_PREVIEW_BASE_CLASSES);
                                    if (fetchedPostElement.classList.contains(SITE_PREVIEW_REPLY_CLASS)) $preview.addClass(SITE_PREVIEW_REPLY_CLASS);
                                    else if (fetchedPostElement.classList.contains(SITE_PREVIEW_OP_CLASS)) $preview.addClass(SITE_PREVIEW_OP_CLASS);
                                    $preview.append(preparePreviewContent(fetchedPostElement));
                                    updatePreviewPosition(e);
                                }
                            } else {
                                if ($preview) $preview.empty().removeClass('loading-preview').addClass('error-preview');
                            }
                        } else {
                            if ($preview) $preview.empty().removeClass('loading-preview').addClass('error-preview');
                        }
                    }).catch(err => {
                        if (controller.signal.aborted) return;
                        fetchController = null;
                        if ($preview) $preview.empty().removeClass('loading-preview').addClass('error-preview');
                    });
            }
        }).on('mouseleave.iqhover', SITE_HOVER_TARGET_SELECTOR, function(e) {
            if (fetchController) { fetchController.abort(); fetchController = null; }
            if ($preview) { $preview.remove(); $preview = null; }
            targetPostId = null;
        }).on('mousemove.iqhover', SITE_HOVER_TARGET_SELECTOR, function(e) {
            clearTimeout(mouseMoveTimer);
            mouseMoveTimer = setTimeout(() => { updatePreviewPosition(e); }, 20);
        });
    }

    function initializeInlineImageHover(parentElement) {
        const $container = $(parentElement);
        let $preview = null;

        function position(e) {
            if (!$preview) return;
            const winW = window.innerWidth, winH = window.innerHeight, el = $preview[0];
            const naturalW = el.naturalWidth || el.videoWidth || 0, naturalH = el.naturalHeight || el.videoHeight || 0; if (!naturalW||!naturalH) return;
            const scale = Math.min(1, (winW*0.97)/naturalW, (winH*0.97)/naturalH);
            const width = naturalW*scale, height = naturalH*scale;
            let left = e.clientX + 45, top = e.clientY - 45;
            if (left + width > winW) left = e.clientX - width - 45;
            if (top + height > winH) top = e.clientY - height;
            if (left < 0) left = 0; if (top < 0) top = 0;
            el.style.width = width + 'px';
            el.style.height = height + 'px';
            el.style.left = left + 'px';
            el.style.top = top + 'px';
        }

        $container.on('mouseenter.iqimagehover', 'a', function(e) {
            let href = this.href; if (!href) return;
            let realHref = href;
            const urlObj = new URL(href, window.location.origin);
            if (urlObj.pathname.endsWith('player.php') && urlObj.searchParams.has('v')) {
                realHref = urlObj.searchParams.get('v');
                if (!/^(https?:)?\/\//i.test(realHref)) realHref = window.location.origin + realHref;
            }
            if (!/\.(jpe?g|png|gif|webp|webm|mp4)(?:\?.*)?$/i.test(realHref)) return;

            const $link = $(this);
            if ($link.data('expanded')) return;
            const isVideo = /\.(webm|mp4)$/i.test(realHref);
            if (isVideo && !iqSettings.enableVideoHoverPreview) return;

            if (isVideo) {
                $preview = $('<video>', {src: realHref, autoplay: true, muted: true, loop: true});
                $preview.addClass('iq-media-hover-preview');
                const vol = getGlobalDefaultVolume();
                $preview.prop('volume', vol);
                $preview.on('loadedmetadata', () => position(e));
            } else {
                $preview = $('<img>', {src: realHref}).addClass('iq-media-hover-preview').on('load', () => position(e));
                $preview.attr('id', 'chx_hoverImage');
            }
            $preview.css({position:'fixed', zIndex:9999, pointerEvents:'none', maxWidth:'97vw', maxHeight:'97vh'}).appendTo('body');
        }).on('mousemove.iqimagehover', 'a', position).on('mouseleave.iqimagehover', 'a', function(){ if($preview) {$preview.remove(); $preview=null;} });
    }

    document.documentElement.addEventListener('click', function(event) {
        const linkElement = event.target.closest('a');
        if (!linkElement) return;
        const postId = getPostIdFromLink(linkElement);
        if (!postId || !/^>>/.test(linkElement.textContent?.trim() || '')) return;
        event.preventDefault(); event.stopImmediatePropagation();
        handleInlineQuoteClick(linkElement, postId);
    }, true);

    $(document).on('mouseenter', SITE_HOVER_TARGET_SELECTOR, function(event) {
        const linkElement = event.currentTarget;
        if ($(linkElement).hasClass(INLINE_ACTIVE_LINK_CLASS)) {
            if ($(event.target).closest(`div[id^="${CLONED_HOVER_PREVIEW_ID_PREFIX}"]`).length > 0) return;
            event.stopImmediatePropagation();
        }
    });
    $(document).on('mouseleave', SITE_HOVER_TARGET_SELECTOR, function(event) {
        const linkElement = event.currentTarget;
        if ($(linkElement).hasClass(INLINE_ACTIVE_LINK_CLASS)) {
            if ($(event.relatedTarget).closest(`div[id^="${CLONED_HOVER_PREVIEW_ID_PREFIX}"]`).length > 0) return;
            event.stopImmediatePropagation();
        }
    });

    function runInitialProcessing() {
        if (!document.body) return;
        processLinks(document.body);
    }
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', runInitialProcessing);
    } else {
        runInitialProcessing();
    }

    const observer = new MutationObserver(mutations => {
        if (!document.body) return;
        mutations.forEach(mutation => {
            if (mutation.addedNodes.length) {
                mutation.addedNodes.forEach(node => {
                    if (node.nodeType === Node.ELEMENT_NODE) {
                        if (node.tagName === 'A' && node.matches(POTENTIAL_QUOTE_LINK_SELECTOR)) {
                            processLinks(node.parentElement || node);
                        } else if (node.querySelector && node.querySelector(POTENTIAL_QUOTE_LINK_SELECTOR)) {
                            processLinks(node);
                        }
                        let containerNode = null;
                        if (node.classList && node.classList.contains(INLINE_CONTAINER_CLASS)) containerNode = node;
                        else if (node.querySelector) containerNode = node.querySelector(`.${INLINE_CONTAINER_CLASS}`);
                        if(containerNode) {
                            const clonedPostElement = containerNode.querySelector(`.${CLONED_POST_CLASS}`);
                            if (clonedPostElement) {
                                initializeInlineHover(clonedPostElement);
                                initializeInlineImageHover(clonedPostElement);
                            }
                        }
                    }
                });
            }
        });
    });
    observer.observe(document.documentElement, { childList: true, subtree: true });
})();