// ==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...
})();