NamuSlack_Hearthstone

편집해야 한다... vs ㅋㅋㅋㅋㅋㅋ (딸깍)

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         NamuSlack_Hearthstone
// @name:ko      나무슬랙_하스스톤
// @namespace    http://tampermonkey.net/
// @version      0.1.1.2
// @description  편집해야 한다... vs ㅋㅋㅋㅋㅋㅋ (딸깍)
// @author       NamuSlack
// @match        https://namu.wiki/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=namu.wiki
// @license      MIT
// @grant        GM_xmlhttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// ==/UserScript==

const fetchTextContent = (url) => {
    return new Promise((resolve, reject) => {
        GM_xmlhttpRequest({
            method: "GET",
            url,
            onload: function(response) {
                resolve(response.responseText); // 파일 내용 반환
            },
            onerror: function(error) {
                reject(error); // 에러 발생시
            }
        });
    });
}

//정규식을 github에 올려서 호출하고 있습니다. 역슬래시의 압박이 너무 심해서 유지보수가 어려워 이렇게 했습니다.
//I'm registering my regular expressions on github. Backslashes are too much of a burden and make maintenance difficult.
let regex = null;

const startEdit = () => {
    GM_setValue("editStep", "start");
    window.location.href = '/edit' + window.location.pathname.slice(2);
}

const removeMarkSyntaxForIncludeArg = (raw) => {
    return raw.replace(/\[\[.*?\|(.*?)\]\]/, "$1").replace(/\[\[(.*?)\]\]/, "$1").replace(/\[\* .*?]/, "").replace(/^\[\[.*?\|/, "");
}

const replaceMarkSyntaxForIncludeArg = (raw) => {
    return raw.replace(/'''(.*?)'''/g, "<b>$1</b>").replace(/''(.*?)''/g, "<i>$1</i>").replace("[br]", "<br>").replaceAll('[[]]','');
};

const exchangePattern = async () => {
    if (!window.location.pathname.startsWith('/w') && !window.location.pathname.startsWith('/edit')) {
        return;
    }

    //window.location.href = '/edit' + window.location.pathname.slice(2);
    const targetButton = document
    .evaluate("//button[normalize-space(text())='RAW 편집']", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null)
    .singleNodeValue;

    if (targetButton) {
        targetButton.click(); // 클릭 이벤트 발생
    }

    const xpath = '//form//textarea[@name]';
    const result = document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
    const textarea = result.singleNodeValue;

    if (textarea) {
        let count = 0;
        textarea.value = textarea.value.replace(regex,
                                                (match, ...args) => {
            let {className, rarity, cardKind, cardSet,
                   koreanName, illust, englishName,
                   cost, attack, healthName, health, species,
                   effect, koreanFlavorText, englishFlavorText,
                   goldLink, goldGain, diamondLink, diamondGain,
                   signatureLink, signatureGain}
            = args.at(-1);
            if ((koreanFlavorText && koreanFlavorText.includes('\n')) || (englishFlavorText && englishFlavorText.includes('\n'))) {
                return match;
            }
            count++;

            const shouldBeRemovedList =
                  [['한글명', koreanName],
                   ['영어명', englishName],
                   ['플레이버한', koreanFlavorText],
                   ['플레이버영', englishFlavorText]];
            let commentOutput = '';
            for (const [label, value] of shouldBeRemovedList) {
                if (!value) {
                    continue;
                }
                const transformed = removeMarkSyntaxForIncludeArg(value);
                if (transformed !== value) {
                    commentOutput += `##${label}: ${value}\n`;
                }
            }

            let macroArgs = [
             `${className === '공용' ? '중립' : className.replace(' ', '')}=`,
             `${rarity ? rarity : '없음'}=`,
             `${cardKind.endsWith("(토큰)") ? cardKind.substr(0, cardKind.length - 4) : cardKind}=`,
             `${'\n'}한글명=${removeMarkSyntaxForIncludeArg(koreanName)}`,
             `일러명=${illust}`,
             `영문명=${removeMarkSyntaxForIncludeArg(englishName)}`,
             `확장팩=${cardSet ? cardSet : '-'}`,
             `${'\n'}비용=${cost}`,
             attack ? `공격력=${attack}` : '',
             health ? `${healthName}=${health}`: '',
             ...(species && species !== '-' ? (species.split('[br]').map(o => `${o}=`)): []),
             `${'\n'}효과=${replaceMarkSyntaxForIncludeArg(effect)}`,
             koreanFlavorText ? `${'\n'}플레이버한=${removeMarkSyntaxForIncludeArg(koreanFlavorText)}` : '',
             englishFlavorText ? `${'\n'}플레이버영=${removeMarkSyntaxForIncludeArg(englishFlavorText)}` : '',
             `${'\n'}황금링크=${goldLink}`,
             `황금획득=${goldGain}`,
             signatureLink ? `${'\n'}간판링크=${signatureLink}` : '',
             signatureGain ? `간판획득=${signatureGain}` : '',
             diamondLink ? `${'\n'}다이아=` : '',
             diamondLink ? `다이아링크=${diamondLink}` : '',
             diamondGain ? `다이아획득=${diamondGain}` : '',
             koreanFlavorText === undefined || koreanFlavorText === null || !koreanFlavorText ? '\n수집불가= ' : ''
            ].filter(item => typeof item === 'string' && item.trim() !== '').map(o => o.replaceAll(',', '\\,')).join(', ').replace(/, \n/g, ',\n');
            const ret = `[include(틀:하스스톤/카드, ${macroArgs})]${'\n'}${commentOutput}`;
            return ret;
        });
        textarea.dispatchEvent(new Event('input', { bubbles: true })); // React 대응

        alert(`${count}개의 템플릿이 교체되었습니다!`);
        const previewButton = Array.from(document.querySelectorAll('button'))
        .find(btn => btn.textContent.trim() === '미리보기');
        if (previewButton) {
            previewButton.click(); // 클릭 이벤트 발생
        }

    }
}

/*const createTextListButton = () => {
    // 초기값
    const STORAGE_KEY = 'myTextList';
    const defaultList = ["예시 1", "예시 2"];

    // 저장된 리스트 가져오기
    const getList = () => GM_getValue(STORAGE_KEY, defaultList);
    const setList = (list) => GM_setValue(STORAGE_KEY, list);

    // 버튼 UI 생성
    const btn = document.createElement('button');
    btn.textContent = "📋 리스트";
    Object.assign(btn.style, {
        position: 'fixed',
        bottom: '10px',
        left: '10px',
        padding: '8px 12px',
        fontSize: '14px',
        background: '#333',
        color: '#fff',
        border: 'none',
        borderRadius: '8px',
        cursor: 'pointer',
        zIndex: 9999,
        opacity: 0.6,
        transition: 'opacity 0.3s'
    });

    btn.addEventListener('mouseover', () =>{ btn.style.opacity = '1'});
    btn.addEventListener('mouseout', () => {btn.style.opacity = '0.6'});

    // 클릭 시 리스트 표시 및 추가 입력 받기
    btn.addEventListener('click', () => {
        const list = getList();
        const current = list.join('\n');
        const updated = prompt("리스트 (한 줄에 하나씩)", current);
        if (updated !== null) {
            const newList = updated.split('\n').map(x => x.trim()).filter(x => x);
            setList(newList);
            alert("✅ 저장 완료!\n\n" + newList.join('\n'));
        }
    });

    document.body.appendChild(btn);
}

const createButton = () => {
    const button = document.createElement('div');
    button.innerText = '▶';
    button.style.position = 'fixed';
    button.style.bottom = '20px';
    button.style.left = '20px';
    button.style.zIndex = '9999';
    button.style.width = '50px';
    button.style.height = '50px';
    button.style.backgroundColor = '#007bff';
    button.style.color = '#fff';
    button.style.borderRadius = '50%';
    button.style.display = 'flex';
    button.style.justifyContent = 'center';
    button.style.alignItems = 'center';
    button.style.boxShadow = '0 4px 8px rgba(0,0,0,0.3)';
    button.style.cursor = 'pointer';
    button.style.opacity = '0.6';
    button.style.transition = 'opacity 0.3s ease';

    // 호버 시 투명도 변화
    button.addEventListener('mouseover', () => {
        button.style.opacity = '1';
    });

    button.addEventListener('mouseout', () => {
        button.style.opacity = '0.6';
    });

    // 클릭 시 함수 실행
    button.addEventListener('click', exchangePattern);

    // 페이지에 버튼 추가
    document.body.appendChild(button);
}*/

function waitForXPath(xpath, callback, timeout = 10000) {
    const start = Date.now();

    const observer = new MutationObserver(() => {
        const result = document.evaluate(
            xpath,
            document,
            null,
            XPathResult.FIRST_ORDERED_NODE_TYPE,
            null
        );
        const node = result.singleNodeValue;

        if (node) {
            observer.disconnect();
            callback(node);
        } else if (Date.now() - start > timeout) {
            observer.disconnect();
            console.warn(`XPath 대기 시간 초과: ${xpath}`);
        }
    });

    observer.observe(document.body, { childList: true, subtree: true });
}


const createEditButton = () => {
    const previewButtonLi = document
    .evaluate("//button[normalize-space(text())='미리보기']/..", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null)
    .singleNodeValue;
    if (previewButtonLi === null){
        console.log("여기선 버튼이 없었습니다.");
        return;
    }
    const newEditButtonLi = previewButtonLi.cloneNode(true);
    previewButtonLi.insertAdjacentElement("afterend", newEditButtonLi);
    const newEditButton = newEditButtonLi.children[0];
    newEditButton.innerText = '하스스톤 템플릿 교체'
    newEditButton.onclick = exchangePattern;
}

(async function() {
    'use strict';

    regex = new RegExp(await fetchTextContent("https://raw.githack.com/CollectiveIntelli/NamuSlack/main/hearthstone.re"), 'gm');
    const runWhenEditPath = () => {
        if(window.location.pathname.startsWith("/edit/")) {
            console.log("edit 페이지에 있습니다.");
            waitForXPath("//button[normalize-space(text())='미리보기']/..", () => {console.log("버튼을 찾았습니다."); createEditButton();});
        }
    };

    // 최초 로딩
    runWhenEditPath();

    //pushState, replaceState 감지용 래핑
    const observeHistory = (type) => {
        const orig = history[type];
        return function(...args) {
            const result = orig.apply(this, args);
            window.dispatchEvent(new Event("locationchange"));
            return result;
        };
    };
    history.pushState = observeHistory("pushState");
    history.replaceState = observeHistory("replaceState");

    // popstate와 custom locationchange 이벤트 감지
    window.addEventListener("popstate", runWhenEditPath);
    window.addEventListener("locationchange", runWhenEditPath);

    //createTextListButton();
    //createButton();
    // 단축키 설정 (예: Ctrl + Shift + Y)
    /*document.addEventListener('keydown', function(e) {
        if (e.ctrlKey && e.key === 'G') {
            e.preventDefault(); // 기본 동작 방지 (선택적)
            exchangePattern();
        }
    });*/
    // Your code here...
})();