您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
주석 번호와 설명을 복사할 수 있는 패널을 작고 세련된 UI로 제공하며, 페이지 내용과 URL 변경 시 자동 갱신, 복사 시 선택된 텍스트에 주석 내용 자동 덧붙임 기능 포함.
// ==UserScript== // @name 나무위키 주석 추출기 // @namespace http://tampermonkey.net/ // @version 2.1 // @description 주석 번호와 설명을 복사할 수 있는 패널을 작고 세련된 UI로 제공하며, 페이지 내용과 URL 변경 시 자동 갱신, 복사 시 선택된 텍스트에 주석 내용 자동 덧붙임 기능 포함. // @match https://namu.wiki/* // @grant GM_setClipboard // ==/UserScript== (function () { 'use strict'; const green = '#4CAF50'; const greenTransparent = 'rgba(76, 175, 80, 0.8)'; let collected = []; let panel, copyBtn, pre; let fixedContainer, smallButton, expandBtn; let isCopyButtonClick = false; // 주석 수집 및 갱신 function collectAndUpdate() { const bodyText = document.body.innerText; const notes = [...bodyText.matchAll(/\[(\d+)\]/g)]; const uniqueNotes = [...new Set(notes.map(m => m[0]))]; const newCollected = []; uniqueNotes.forEach(n => { const parts = bodyText.split(n); if (parts.length >= 3) { const afterSecond = parts[2].trim(); const nextLine = afterSecond.split(/\n/)[0]; newCollected.push(`${n} ${nextLine}`); } }); if (JSON.stringify(newCollected) !== JSON.stringify(collected)) { collected = newCollected; pre.innerText = collected.length > 0 ? collected.join('\n') : '(복사할 주석이 없습니다)'; } } // 텍스트 내 주석 번호를 주석 내용으로 대체 (텍스트 전용) function replaceNotesWithContents(text) { const noteMap = {}; collected.forEach(line => { const m = line.match(/^\[(\d+)\]\s*(.*)$/); if (m) noteMap[m[1]] = m[2]; }); return text.replace(/\[(\d+)\]/g, (match, num) => noteMap[num] ? `[${noteMap[num]}]` : match ); } // 복사용 DOM 노드 내 주석 대체, 링크 내 주석 텍스트로만 대체하고 링크는 제거 // 단, 링크 내 주석 없으면 링크 유지 function replaceNotesInNode(node, noteMap) { if (node.nodeType === Node.TEXT_NODE) { node.textContent = node.textContent.replace(/\[(\d+)\]/g, (match, num) => noteMap[num] ? `[${noteMap[num]}]` : match ); } else if (node.nodeType === Node.ELEMENT_NODE) { if (node.tagName === 'A') { const hasNote = /\[(\d+)\]/.test(node.textContent); if (hasNote) { const replacedText = node.textContent.replace(/\[(\d+)\]/g, (match, num) => noteMap[num] ? `[${noteMap[num]}]` : match ); const textNode = document.createTextNode(replacedText); node.parentNode.replaceChild(textNode, node); } else { node.childNodes.forEach(child => replaceNotesInNode(child, noteMap)); } } else { node.childNodes.forEach(child => replaceNotesInNode(child, noteMap)); } } } // 임시 복사 DOM 내 a[href] 절대경로 변환 (상대경로만 변환) function fixLinksToAbsolute(container) { const anchors = container.querySelectorAll('a[href]'); anchors.forEach(a => { const href = a.getAttribute('href'); if (href && !href.match(/^https?:\/\//i) && !href.startsWith('mailto:') && !href.startsWith('#')) { try { const absUrl = new URL(href, location.origin); a.setAttribute('href', absUrl.href); } catch {} } }); } // 공통 복사 함수 function copyNotes() { const selection = window.getSelection(); const textToCopy = selection && selection.toString().trim().length > 0 ? replaceNotesWithContents(selection.toString()) : collected.join('\n'); if (textToCopy) { GM_setClipboard(textToCopy); return true; } return false; } // 복사 버튼 클릭 처리 함수 (버튼 요소 인자로 받음) function handleCopyClick(button) { isCopyButtonClick = true; if (copyNotes()) { const originalText = button.innerText; button.innerText = '복사됨!'; setTimeout(() => { button.innerText = originalText; isCopyButtonClick = false; }, 1500); } else { isCopyButtonClick = false; } } function createUI() { panel = document.createElement('div'); panel.style = ` position: fixed; bottom: 20px; right: 20px; z-index: 9999; background: white; border: 1px solid #ccc; padding: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.2); max-width: 300px; max-height: 400px; overflow: auto; font-size: 13px; line-height: 1.4; display: none; `; copyBtn = document.createElement('button'); copyBtn.innerText = '주석 복사'; copyBtn.style = ` background: ${green}; color: white; border: none; padding: 6px 12px; font-size: 13px; border-radius: 4px; cursor: pointer; `; copyBtn.onclick = () => { handleCopyClick(copyBtn); }; pre = document.createElement('pre'); pre.style.whiteSpace = 'pre-wrap'; pre.innerText = '(복사할 주석이 없습니다)'; const collapseBtn = document.createElement('div'); collapseBtn.innerHTML = ` <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="white" viewBox="0 0 24 24"> <path d="M19 13H5v-2h14v2z"/> </svg>`; collapseBtn.style = ` position: absolute; top: 6px; right: 6px; width: 20px; height: 20px; background: rgba(255, 0, 0, 0.8); border-radius: 4px; display: flex; align-items: center; justify-content: center; cursor: pointer; `; collapseBtn.onclick = () => { panel.style.display = 'none'; fixedContainer.style.display = 'flex'; }; panel.appendChild(copyBtn); panel.appendChild(document.createElement('hr')); panel.appendChild(pre); panel.appendChild(collapseBtn); fixedContainer = document.createElement('div'); fixedContainer.style = ` position: fixed; bottom: 20px; right: 20px; z-index: 9998; display: flex; align-items: center; gap: 4px; `; smallButton = document.createElement('button'); smallButton.innerText = '주석 복사'; smallButton.style = ` background: ${green}; color: white; border: none; padding: 6px 12px; font-size: 13px; border-radius: 4px; cursor: pointer; flex-shrink: 0; height: 28px; `; smallButton.onclick = () => { handleCopyClick(smallButton); }; expandBtn = document.createElement('button'); expandBtn.title = '펼치기'; expandBtn.style = ` background: ${greenTransparent}; border: none; border-radius: 4px; height: 28px; width: 28px; padding: 0; display: flex; align-items: center; justify-content: center; cursor: pointer; `; expandBtn.innerHTML = ` <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="white" viewBox="0 0 24 24"> <path d="M4 4h6v2H6v4H4V4zm16 0v6h-2V6h-4V4h6zm0 16h-6v-2h4v-4h2v6zM4 20v-6h2v4h4v2H4z"/> </svg>`; expandBtn.onclick = () => { panel.style.display = 'block'; fixedContainer.style.display = 'none'; }; fixedContainer.appendChild(smallButton); fixedContainer.appendChild(expandBtn); document.body.appendChild(panel); document.body.appendChild(fixedContainer); } // SPA 대응 URL 변경 감지 function onUrlChange(callback) { window.addEventListener('popstate', callback); const pushState = history.pushState; history.pushState = function (...args) { const result = pushState.apply(this, args); callback(); return result; }; const replaceState = history.replaceState; history.replaceState = function (...args) { const result = replaceState.apply(this, args); callback(); return result; }; } createUI(); collectAndUpdate(); const observer = new MutationObserver(() => { collectAndUpdate(); }); observer.observe(document.body, { childList: true, subtree: true, characterData: true }); onUrlChange(() => { setTimeout(() => { collectAndUpdate(); }, 500); }); // 복사 시 주석 내용 자동 덧붙임 및 하이퍼링크 처리 document.addEventListener('copy', (e) => { if (isCopyButtonClick) { // 주석창의 복사 버튼 클릭 시는 가로채지 않고 원본 복사 허용 return; } // ✅ 주석창이 열려있으면 원래 복사만 허용 if (panel && panel.style.display !== 'none') { return; // 패널이 열려있을 때는 기본 복사 } // ✅ 주석창이 닫혀있을 때만 주석 변환 가로채기 const selection = window.getSelection(); if (!selection || selection.rangeCount === 0) return; const range = selection.getRangeAt(0); const cloned = range.cloneContents(); const noteMap = {}; collected.forEach(line => { const m = line.match(/^\[(\d+)\]\s*(.*)$/); if (m) noteMap[m[1]] = m[2]; }); const div = document.createElement('div'); div.appendChild(cloned); replaceNotesInNode(div, noteMap); fixLinksToAbsolute(div); const selectedText = selection.toString(); const replacedText = replaceNotesWithContents(selectedText); e.preventDefault(); e.clipboardData.setData('text/html', div.innerHTML); e.clipboardData.setData('text/plain', replacedText); }); })();