您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
現在のページのHTMLを整形してUTF-8で保存。
当前为
// ==UserScript== // @name HTML Source Downloader // @namespace https://bsky.app/profile/neon-ai.art // @homepage https://bsky.app/profile/neon-ai.art // @icon data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>⛄️</text></svg> // @description 現在のページのHTMLを整形してUTF-8で保存。 // @author ねおん // @version 3.0 // @match *://*/* // @grant GM_getValue // @grant GM_setValue // @grant GM_addStyle // @grant GM_registerMenuCommand // @license CC BY-NC 4.0 // ==/UserScript== (function () { 'use strict'; const VERSION = 'v3.0'; if (window.top !== window.self) return; // サブフレームでは動かさない // ========= 設定 ========= const STORE_KEY = 'html_source_dl__shortcut'; let userShortcut = GM_getValue(STORE_KEY, 'Alt+Shift+S'); // 全ドメイン共通 // ========= ユーティリティ ========= const VOID_RE = /^(?:area|base|br|col|embed|hr|img|input|keygen|link|meta|param|source|track|wbr)\b/i; function normalizeShortcutString(s) { if (!s) return 'Alt+Shift+S'; s = String(s).trim().replace(/\s+/g, ''); // 分解(+区切り、修飾キーは大小不問) const parts = s.split('+').map(p => p.toLowerCase()); const mods = new Set(); let main = ''; for (const p of parts) { if (['ctrl', 'control'].includes(p)) mods.add('Ctrl'); else if (['alt', 'option'].includes(p)) mods.add('Alt'); else if (['shift'].includes(p)) mods.add('Shift'); else if (['meta', 'cmd', 'command', '⌘'].includes(p)) mods.add('Meta'); else main = p; } // メインキーを大文字1文字 or F1.. の形に if (!main) main = 'S'; if (/^key[a-z]$/i.test(main)) main = main.slice(3); // "KeyK" if (/^digit[0-9]$/i.test(main)) main = main.slice(5); // "Digit1" if (/^[a-z]$/.test(main)) main = main.toUpperCase(); if (/^f([1-9]|1[0-2])$/i.test(main)) main = main.toUpperCase(); // 記号などはそのまま(例: Slash, Backquote などの code 名) const order = ['Ctrl', 'Shift', 'Alt', 'Meta']; const modStr = order.filter(m => mods.has(m)).join('+'); return (modStr ? modStr + '+' : '') + main; } function eventMatchesShortcut(e, shortcut) { const norm = normalizeShortcutString(shortcut); const parts = norm.split('+'); const mods = new Set(parts.slice(0, -1)); const keyPart = parts[parts.length - 1]; const need = { Ctrl: mods.has('Ctrl'), Shift: mods.has('Shift'), Alt: mods.has('Alt'), Meta: mods.has('Meta'), }; if (need.Ctrl !== e.ctrlKey) return false; if (need.Shift !== e.shiftKey) return false; if (need.Alt !== e.altKey) return false; if (need.Meta !== e.metaKey) return false; // メインキー判定(英数字は e.code 基準) const main = keyPart; let pressed = ''; if (e.code.startsWith('Key')) pressed = e.code.slice(3).toUpperCase(); else if (e.code.startsWith('Digit')) pressed = e.code.slice(5); else pressed = e.key.length === 1 ? e.key.toUpperCase() : e.key; // F1 などは e.key return pressed === main; } // ========= ダウンロード本体 ========= function downloadHTML() { try { const d = document; const dt = d.doctype ? `<!DOCTYPE ${d.doctype.name}${ d.doctype.publicId ? ` PUBLIC "${d.doctype.publicId}"` : '' }${!d.doctype.publicId && d.doctype.systemId ? ' SYSTEM' : ''}${ d.doctype.systemId ? ` "${d.doctype.systemId}"` : '' }>\n` : ''; let html = dt + d.documentElement.outerHTML; // meta charset を UTF-8 に統一 if (/<meta[^>]*charset\s*=\s*["']?[^"'>\s]+["']?[^>]*>/i.test(html)) { html = html.replace( /<meta[^>]*charset\s*=\s*["']?[^"'>\s]+["']?[^>]*>/i, '<meta charset="UTF-8">' ); } else if (/<head[^>]*>/i.test(html)) { html = html.replace(/<head[^>]*>/i, '$&<meta charset="UTF-8">'); } else { html = '<meta charset="UTF-8">' + html; } // 整形 html = (p => { let i = 0; return p .replace(/>\s*</g, '><') .replace(/></g, '>\n<') .split('\n') .map(l => { if (/^<\//.test(l) && !/.*<\/.+>.*<.+>/.test(l)) i = Math.max(i - 1, 0); const r = ' '.repeat(Math.max(i, 0)) + l; if ( /^<[^!?/]/.test(l) && !/<.+<\/.+>/.test(l) && !/\/>$/.test(l) && !VOID_RE.test(l) ){ i++; } return r; }) .join('\n'); })(html); // ファイル名 const pad = n => String(n).padStart(2, '0'); const now = new Date(); const ts = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad( now.getDate() )}_${pad(now.getHours())}-${pad(now.getMinutes())}-${pad( now.getSeconds() )}`; const path = (location.pathname || '/') .replace(/\/+/g, '/') .replace(/[^a-z0-9\-_.\/]/gi, '_') .replace(/^\/|\/$/g, '') .replace(/\//g, '_') || 'index'; const name = (location.hostname || 'page') + '_' + path + '_' + ts + '.html'; // ダウンロード try { const blob = new Blob([html], { type: 'text/html;charset=utf-8' }); const a = d.createElement('a'); a.download = name; a.href = URL.createObjectURL(blob); (d.body || d.documentElement).appendChild(a); a.click(); setTimeout(() => { URL.revokeObjectURL(a.href); a.remove(); }, 1000); } catch (err) { // 失敗時は別タブ(data:) const url = 'data:text/html;charset=utf-8,' + encodeURIComponent(html); const w = window.open(url); if (!w) alert('ポップアップがブロックされました。許可してからもう一度試してね'); } } catch (e) { alert('Failed: ' + e); } } // ========= 設定UI ========= function ensureStyle() { if (document.getElementById('hsd-style')) return; const style = document.createElement('style'); style.id = 'hsd-style'; style.textContent = ` :root { --bg-color: #1a1a1a; --text-color: #f0f0f0; --border-color: #333; --primary-color: #007bff; --primary-hover: #0056b3; --secondary-color: #343a40; --modal-bg: #212529; --shadow: 0 8px 16px rgba(0, 0, 0, 0.5); --border-radius: 12px; } .hsd-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.7); z-index: 100000; display: flex; justify-content: center; align-items: center; } .hsd-panel { background-color: var(--modal-bg); color: var(--text-color); width: 90%; max-width: 400px; border-radius: var(--border-radius); box-shadow: var(--shadow); border: 1px solid var(--border-color); font-family: 'Inter', sans-serif; overflow: hidden; } .hsd-title { padding: 15px 20px; display: flex; align-items: center; justify-content: space-between; border-bottom: 1px solid var(--border-color); font-size: 1.25rem; font-weight: 600; margin: 0; } .hsd-close { background: none; border: none; cursor: pointer; font-size: 24px; color: var(--text-color); opacity: 0.7; padding: 0; } .hsd-close:hover { opacity: 1; } .hsd-section { padding: 20px; } .hsd-label { font-size: 1rem; font-weight: 500; color: #e0e0e0; display: block; margin-bottom: 8px; } .hsd-input { width: 100%; padding: 8px 12px; background-color: var(--secondary-color); color: var(--text-color); border: 1px solid var(--border-color); border-radius: 6px; cursor: text; box-sizing: border-box; } .hsd-input:focus { border-color: var(--primary-color); box-shadow: 0 0 4px var(--primary-color); } .hsd-bottom { padding: 15px 20px; border-top: 1px solid var(--border-color); display: flex; justify-content: space-between; align-items: center; } .hsd-bottom .hsd-version { font-size: 0.8rem; font-weight: 400; color: #aaa; } .hsd-button { padding: 10px 20px; border: none; border-radius: 6px; font-weight: bold; cursor: pointer; transition: all 0.2s ease; background-color: var(--primary-color); color: white; } .hsd-button:hover { background-color: var(--primary-hover); } `; document.head.appendChild(style); } function showToast(msg) { const toast = document.createElement('div'); toast.textContent = msg; toast.style.cssText = ` position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%); background: var(--primary-color); color: white; padding: 10px 20px; border-radius: 6px; z-index: 100000; font-size: 14px; `; document.body.appendChild(toast); setTimeout(() => toast.remove(), 2000); } function openSettings() { ensureStyle(); // モーダル背景 const overlay = document.createElement('div'); overlay.className = 'hsd-overlay'; // モーダル本体 const panel = document.createElement('div'); panel.className = 'hsd-panel'; // 閉じるボタン const closeBtn = document.createElement('span'); closeBtn.className = 'hsd-close'; closeBtn.textContent = '×'; closeBtn.title = '閉じる'; closeBtn.addEventListener('click', () => document.body.removeChild(overlay)); // タイトルバー const title = document.createElement('div'); title.className = 'hsd-title'; title.textContent = '設定'; title.appendChild(closeBtn); // 設定セクション const section = document.createElement('div'); section.className = 'hsd-section'; const label = document.createElement('div'); label.className = 'hsd-label'; label.textContent = 'ショートカットキー'; const input = document.createElement('input'); input.type = 'text'; input.className = 'hsd-input'; input.placeholder = '例: Ctrl+Shift+K'; input.setAttribute("inputmode", "latin"); input.setAttribute("lang", "en"); input.inputMode = 'latin'; input.style.imeMode = "disabled"; input.readOnly = true; input.addEventListener('input', () => { // 全角英数字→半角英数字に変換 input.value = input.value.replace(/[A-Za-z0-9]/g, s => String.fromCharCode(s.charCodeAt(0) - 0xFEE0) ); // 全角記号や日本語を削除 input.value = input.value.replace(/[^\x00-\x7F]/g, ''); }); input.value = normalizeShortcutString(userShortcut); // キーキャプチャ(小文字入力でも大文字表示) input.addEventListener('keydown', e => { e.preventDefault(); // IME入力や全角候補を完全ブロック const mods = []; if (e.ctrlKey) mods.push('Ctrl'); if (e.shiftKey) mods.push('Shift'); if (e.altKey) mods.push('Alt'); if (e.metaKey) mods.push('Meta'); let main = ''; if (e.code.startsWith('Key')) main = e.code.slice(3).toUpperCase(); else if (e.code.startsWith('Digit')) main = e.code.slice(5); else if (/^F[1-9]|F1[0-2]$/.test(e.key)) main = e.key.toUpperCase(); else if (e.key && e.key.length === 1) main = e.key.toUpperCase(); // ここで value を上書き input.value = (mods.length ? mods.join('+') + '+' : '') + main; }); section.appendChild(label); section.appendChild(input); // フッター(バージョン & 保存ボタン) const bottom = document.createElement('div'); bottom.className = 'hsd-bottom'; const version = document.createElement('div'); version.className = 'hsd-version'; version.textContent = '(' + VERSION + ')'; const saveBtn = document.createElement('button'); saveBtn.className = 'hsd-button'; saveBtn.textContent = '保存'; saveBtn.addEventListener('click', () => { const norm = normalizeShortcutString(input.value); userShortcut = norm; GM_setValue(STORE_KEY, userShortcut); // 全ドメイン共通保存 document.body.removeChild(overlay); showToast('設定を保存しました!'); }); bottom.appendChild(version); bottom.appendChild(saveBtn); // モーダル背景クリックで閉じる overlay.addEventListener('click', e => { if (e.target === overlay) overlay.remove(); }); // ESCキーで閉じる document.addEventListener('keydown', e => { if (e.key === 'Escape') overlay.remove(); }, { once: true }); // 組み立て panel.appendChild(title); panel.appendChild(section); panel.appendChild(bottom); overlay.appendChild(panel); document.body.appendChild(overlay); input.focus(); } // ========= イベント / メニュー ========= // メニュー(実行 & 設定) GM_registerMenuCommand('HTMLをダウンロード', downloadHTML); GM_registerMenuCommand('設定', openSettings); // ショートカット実行 document.addEventListener('keydown', e => { // 入力欄でのタイプは無視(フォーム操作の邪魔をしない) const tag = (e.target && e.target.tagName) || ''; if (/(INPUT|TEXTAREA|SELECT)/.test(tag)) return; if (eventMatchesShortcut(e, userShortcut)) { e.preventDefault(); downloadHTML(); } }); })();