Skip to content

Instantly share code, notes, and snippets.

@zhuziyi1989
Last active October 21, 2025 17:27
Show Gist options
  • Save zhuziyi1989/c728adf9c1d569e74362ca00818fe979 to your computer and use it in GitHub Desktop.
Save zhuziyi1989/c728adf9c1d569e74362ca00818fe979 to your computer and use it in GitHub Desktop.

Revisions

  1. zhuziyi1989 revised this gist Oct 21, 2025. 1 changed file with 2 additions and 2 deletions.
    4 changes: 2 additions & 2 deletions v2ex-base64-enhance.js
    Original file line number Diff line number Diff line change
    @@ -10,8 +10,8 @@
    // @run-at document-idle
    // @supportURL https://gist.github.com/zhuziyi1989/c728adf9c1d569e74362ca00818fe979
    // @homepageURL https://gist.github.com/zhuziyi1989/c728adf9c1d569e74362ca00818fe979
    // @downloadURL https://gist.github.com/zhuziyi1989/c728adf9c1d569e74362ca00818fe979/raw/81f269008d5d9e137a04746a9582c21d68a52a45/v2ex-base64-enhance.js
    // @updateURL https://gist.github.com/zhuziyi1989/c728adf9c1d569e74362ca00818fe979/raw/81f269008d5d9e137a04746a9582c21d68a52a45/v2ex-base64-enhance.js
    // @downloadURL https://gist.github.com/zhuziyi1989/c728adf9c1d569e74362ca00818fe979/raw/v2ex-base64-enhance.js
    // @updateURL https://gist.github.com/zhuziyi1989/c728adf9c1d569e74362ca00818fe979/raw/v2ex-base64-enhance.js
    // ==/UserScript==

    (function () {
  2. zhuziyi1989 revised this gist Oct 17, 2025. 1 changed file with 0 additions and 389 deletions.
    389 changes: 0 additions & 389 deletions V2EX Base64 Enhance.js
    Original file line number Diff line number Diff line change
    @@ -1,389 +0,0 @@
    // ==UserScript==
    // @name V2EX Base64 Enhance 自动解析增强版
    // @namespace http://tampermonkey.net/
    // @version 1.3
    // @description 在 V2EX 帖子详情页自动发现 Base64 字符串并在其后追加解码结果;若解码为 URL 则渲染为可点击链接(在新标签打开)。仅处理帖子正文与回复内容,避免页面卡死。
    // @author zhuziyi
    // @match https://v2ex.com/t/*
    // @match https://*.v2ex.com/t/*
    // @grant none
    // @run-at document-idle
    // @supportURL https://gist.github.com/zhuziyi1989/c728adf9c1d569e74362ca00818fe979
    // @homepageURL https://gist.github.com/zhuziyi1989/c728adf9c1d569e74362ca00818fe979
    // @downloadURL https://gist.github.com/zhuziyi1989/c728adf9c1d569e74362ca00818fe979/raw/81f269008d5d9e137a04746a9582c21d68a52a45/v2ex-base64-enhance.js
    // @updateURL https://gist.github.com/zhuziyi1989/c728adf9c1d569e74362ca00818fe979/raw/81f269008d5d9e137a04746a9582c21d68a52a45/v2ex-base64-enhance.js
    // ==/UserScript==

    (function () {
    "use strict";

    const MIN_B64_LEN = 16; // 最小 Base64 长度阈值,按需调整
    const SKIP_TAGS = new Set([
    "SCRIPT",
    "STYLE",
    "CODE",
    "PRE",
    "TEXTAREA",
    "INPUT",
    "A",
    ]);
    const B64_RE = new RegExp(
    `([A-Za-z0-9_\\-+\\/]{${MIN_B64_LEN},}={0,2})`,
    "g"
    );
    const APP_MARK = "data-b64-script"; // 我们插入节点时会打上这个标记

    // 记录已处理过的文本节点,避免重复处理
    const processedTextNodes = new WeakSet();

    function normalizeBase64(b64) {
    let s = b64.replace(/\s+/g, "");
    s = s.replace(/-/g, "+").replace(/_/g, "/");
    const pad = s.length % 4;
    if (pad === 1) return null;
    if (pad !== 0) s += "=".repeat(4 - pad);
    return s;
    }

    function decodeBase64ToString(b64) {
    try {
    const norm = normalizeBase64(b64);
    if (!norm) return null;
    const binary = atob(norm);
    // binary 是 Latin1,需要转为 UTF-8
    let percentEncoded = "";
    for (let i = 0; i < binary.length; i++) {
    const code = binary.charCodeAt(i);
    percentEncoded += "%" + ("00" + code.toString(16)).slice(-2);
    }
    try {
    return decodeURIComponent(percentEncoded);
    } catch (e) {
    // 退回为原 binary(可能是 Latin1 文本)
    return binary;
    }
    } catch (e) {
    return null;
    }
    }

    function looksReadable(s) {
    if (!s || s.length < 3) return false;
    if (/[\u4e00-\u9fff]/.test(s)) return true; // 含中文
    if (/(https?:\/\/|www\.)/i.test(s)) return true;
    // 至少有 60% 的可打印 ASCII 字符
    const printableCount = (s.match(/[\x20-\x7E]/g) || []).length;
    if (printableCount / s.length >= 0.6) return true;
    return false;
    }

    function createAppendNode(decodedText) {
    const wrapper = document.createElement("span");
    wrapper.setAttribute(APP_MARK, "1");
    wrapper.style.marginLeft = "6px";
    wrapper.style.fontSize = "0.95em";
    wrapper.style.verticalAlign = "baseline";

    const trimmed = decodedText.trim();
    const isURL = /^https?:\/\//i.test(trimmed);

    if (isURL) {
    // 全角括号包裹的可点链接
    const left = document.createTextNode("(");
    const right = document.createTextNode(")");

    const a = document.createElement("a");
    a.href = trimmed;
    a.target = "_blank";
    a.rel = "noopener noreferrer";
    a.textContent = trimmed;
    // 不强制设置颜色,保持与站点主题兼容;若需自定义可修改以下两行
    a.style.textDecoration = "underline";
    a.style.cursor = "pointer";

    wrapper.appendChild(left);
    wrapper.appendChild(a);
    wrapper.appendChild(right);
    } else {
    wrapper.style.color = "#888";
    wrapper.textContent = `(${trimmed})`;
    }

    return wrapper;
    }

    function processTextNode(textNode) {
    if (!textNode || !textNode.nodeValue) return;
    if (processedTextNodes.has(textNode)) return;

    const parent = textNode.parentNode;
    if (!parent || SKIP_TAGS.has(parent.tagName)) return;
    // 如果父元素或祖先已是我们插入内容的标记,则跳过(避免在我们的 span 内再次处理)
    if (parent.closest && parent.closest("[" + APP_MARK + "]")) return;

    const text = textNode.nodeValue;
    B64_RE.lastIndex = 0;
    let match;
    let lastIndex = 0;
    const frag = document.createDocumentFragment();
    let anyMatch = false;

    while ((match = B64_RE.exec(text)) !== null) {
    const b64 = match[1];
    // 把前面的纯文本加入
    const before = text.slice(lastIndex, match.index);
    if (before) frag.appendChild(document.createTextNode(before));

    // 原 Base64 文本(保持原样)
    const origNode = document.createTextNode(b64);
    frag.appendChild(origNode);

    // 尝试解码并判断是否可读
    const decoded = decodeBase64ToString(b64);
    if (decoded && looksReadable(decoded)) {
    const appendNode = createAppendNode(decoded);
    frag.appendChild(appendNode);
    }

    lastIndex = match.index + b64.length;
    anyMatch = true;
    }

    if (!anyMatch) {
    // 标记为已检查过(避免下次又检查同一节点)
    processedTextNodes.add(textNode);
    return;
    }

    // 尾部文本
    const tail = text.slice(lastIndex);
    if (tail) frag.appendChild(document.createTextNode(tail));

    // 替换原 text node(注意:替换后原节点不再存在)
    try {
    parent.replaceChild(frag, textNode);
    } catch (e) {
    // 如果替换失败(极少见),仍将该节点标记以避免重复
    processedTextNodes.add(textNode);
    return;
    }
    // 无需标记新创建的文本节点;但为了避免再次检查同一位置,我们可以标记父元素(轻量)
    // 在父元素上打一个短期属性,表明它已经被扫描(不会影响页面)
    try {
    parent.setAttribute("data-b64-scanned", Date.now().toString());
    } catch (e) {
    // 忽略不可设置属性的情况(例如某些 SVG 节点)
    }
    }

    function walkAndProcess(root) {
    if (!root) return;
    const walker = document.createTreeWalker(
    root,
    NodeFilter.SHOW_TEXT,
    {
    acceptNode(node) {
    if (!node.nodeValue || !node.nodeValue.trim())
    return NodeFilter.FILTER_REJECT;
    const p = node.parentNode;
    if (!p) return NodeFilter.FILTER_REJECT;
    if (SKIP_TAGS.has(p.tagName)) return NodeFilter.FILTER_REJECT;
    if (p.closest && p.closest("[" + APP_MARK + "]"))
    return NodeFilter.FILTER_REJECT;
    if (processedTextNodes.has(node)) return NodeFilter.FILTER_REJECT;
    return NodeFilter.FILTER_ACCEPT;
    },
    },
    false
    );

    const nodes = [];
    let n;
    while ((n = walker.nextNode())) nodes.push(n);
    for (const tn of nodes) processTextNode(tn);
    }

    // 找到需要处理的区域(帖子正文与回复)
    function findTargets() {
    const nodes = [];
    const main = document.querySelector(".topic_content");
    if (main) nodes.push(main);
    // 回复区常见类名
    document
    .querySelectorAll(".reply_content, .message, .topic .cell .reply_content")
    .forEach((n) => {
    if (n) nodes.push(n);
    });
    return nodes;
    }

    function initScan() {
    const targets = findTargets();
    if (targets.length === 0) return;
    for (const t of targets) walkAndProcess(t);
    }

    // 监听帖子区域的变化(只对目标区域设置 observer)
    const observers = [];
    function initObservers() {
    const targets = findTargets();
    if (targets.length === 0) return;

    for (const t of targets) {
    const mo = new MutationObserver((mutations) => {
    for (const m of mutations) {
    // 跳过我们自己插入的节点(根节点带标记)
    if (m.addedNodes && m.addedNodes.length) {
    for (const node of m.addedNodes) {
    if (node.nodeType === Node.ELEMENT_NODE) {
    // 如果新增节点是我们自己创建的标记子树,跳过
    if (node.hasAttribute && node.hasAttribute(APP_MARK)) continue;
    // 如果新增元素内部含有我们的标记,也跳过
    if (
    node.querySelector &&
    node.querySelector("[" + APP_MARK + "]")
    )
    continue;
    // 对新增元素内部做扫描
    walkAndProcess(node);
    } else if (node.nodeType === Node.TEXT_NODE) {
    // 直接处理文本节点
    processTextNode(node);
    }
    }
    }
    if (m.type === "characterData" && m.target) {
    processTextNode(m.target);
    }
    }
    });
    mo.observe(t, { childList: true, subtree: true, characterData: true });
    observers.push(mo);
    }
    }

    // 初次运行
    initScan();
    initObservers();

    // 暴露一个手动触发的函数(控制台可用),方便调试/手动重新扫描
    window.__v2ex_b64_rescan = function () {
    initScan();
    console.log("v2ex-base64: manual rescan done");
    };

    console.log("v2ex-base64 (post-only) loaded");

    // ============ 新增:Base64 加密与解密 选中文本 ============

    // Base64 编码
    function encodeBase64(str) {
    try {
    return btoa(unescape(encodeURIComponent(str)));
    } catch (e) {
    return str;
    }
    }

    // Base64 解码
    function decodeBase64(str) {
    try {
    return decodeURIComponent(escape(atob(str)));
    } catch (e) {
    return str;
    }
    }

    // ============ 新:自动隐藏按钮 ============

    function updateButtonVisibility() {
    const textarea = document.querySelector("#reply_content");
    const encodeBtn = document.getElementById("base64_encode_btn");
    const decodeBtn = document.getElementById("base64_decode_btn");
    if (!textarea || !encodeBtn || !decodeBtn) return;

    const start = textarea.selectionStart;
    const end = textarea.selectionEnd;

    // 无选区 → 按钮隐藏,但保留占位
    if (start === end) {
    encodeBtn.style.visibility = "hidden";
    decodeBtn.style.visibility = "hidden";
    } else {
    encodeBtn.style.visibility = "visible";
    decodeBtn.style.visibility = "visible";
    }
    }

    function addBase64Buttons() {
    const replyBtn = document.querySelector(
    'input[type="submit"][value="回复"]'
    );
    replyBtn.parentElement.style.justifyContent = "flex-start";
    replyBtn.parentElement.style.flexDirection = "row-reverse";
    const textarea = document.querySelector("#reply_content");

    if (!replyBtn || !textarea) return;
    if (document.getElementById("base64_encode_btn")) return;

    // 按钮创建器
    function createBtn(text, id) {
    const btn = document.createElement("button");
    btn.id = id;
    btn.textContent = text;
    btn.style.marginRight = "8px";
    btn.style.padding = "5px 8px";
    btn.style.border = "1px solid #f5f5f5";
    btn.style.background = "#f5f5f5";
    btn.style.borderRadius = "5px";
    btn.style.cursor = "pointer";
    btn.style.fontSize = "12px";
    btn.style.lineHeight = "1.4";
    btn.style.color = "#9349ff";
    btn.style.visibility = "hidden"; // 初始隐藏
    return btn;
    }

    const encodeBtn = createBtn("🔐 Base64编码", "base64_encode_btn");
    encodeBtn.addEventListener("click", (e) => {
    e.preventDefault();
    const start = textarea.selectionStart;
    const end = textarea.selectionEnd;
    const txt = textarea.value.substring(start, end);
    textarea.value =
    textarea.value.substring(0, start) +
    encodeBase64(txt) +
    textarea.value.substring(end);
    textarea.selectionStart = textarea.selectionEnd =
    start + encodeBase64(txt).length;
    textarea.focus();
    updateButtonVisibility();
    });

    const decodeBtn = createBtn("🔓 Base64解码", "base64_decode_btn");
    decodeBtn.addEventListener("click", (e) => {
    e.preventDefault();
    const start = textarea.selectionStart;
    const end = textarea.selectionEnd;
    const txt = textarea.value.substring(start, end);
    textarea.value =
    textarea.value.substring(0, start) +
    decodeBase64(txt) +
    textarea.value.substring(end);
    textarea.selectionStart = textarea.selectionEnd =
    start + decodeBase64(txt).length;
    textarea.focus();
    updateButtonVisibility();
    });

    replyBtn.parentNode.insertBefore(encodeBtn, replyBtn.nextSibling);
    replyBtn.parentNode.insertBefore(decodeBtn, encodeBtn.nextSibling);

    // 监听 selection 改变 → 控制可见性
    ["select", "keyup", "mouseup"].forEach((evt) => {
    textarea.addEventListener(evt, updateButtonVisibility);
    });
    }

    setTimeout(addBase64Buttons, 1500);
    })();
  3. zhuziyi1989 revised this gist Oct 17, 2025. 1 changed file with 2 additions and 2 deletions.
    4 changes: 2 additions & 2 deletions v2ex-base64-enhance.js
    Original file line number Diff line number Diff line change
    @@ -10,8 +10,8 @@
    // @run-at document-idle
    // @supportURL https://gist.github.com/zhuziyi1989/c728adf9c1d569e74362ca00818fe979
    // @homepageURL https://gist.github.com/zhuziyi1989/c728adf9c1d569e74362ca00818fe979
    // @downloadURL https://gist.github.com/zhuziyi1989/c728adf9c1d569e74362ca00818fe979/raw/97adee1329d4de9f6ef21eeb500006dea9998508/V2EX%2520Base64%2520Enhance.js
    // @updateURL https://gist.github.com/zhuziyi1989/c728adf9c1d569e74362ca00818fe979/raw/97adee1329d4de9f6ef21eeb500006dea9998508/V2EX%2520Base64%2520Enhance.js
    // @downloadURL https://gist.github.com/zhuziyi1989/c728adf9c1d569e74362ca00818fe979/raw/81f269008d5d9e137a04746a9582c21d68a52a45/v2ex-base64-enhance.js
    // @updateURL https://gist.github.com/zhuziyi1989/c728adf9c1d569e74362ca00818fe979/raw/81f269008d5d9e137a04746a9582c21d68a52a45/v2ex-base64-enhance.js
    // ==/UserScript==

    (function () {
  4. zhuziyi1989 revised this gist Oct 17, 2025. 1 changed file with 389 additions and 0 deletions.
    389 changes: 389 additions & 0 deletions V2EX Base64 Enhance.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,389 @@
    // ==UserScript==
    // @name V2EX Base64 Enhance 自动解析增强版
    // @namespace http://tampermonkey.net/
    // @version 1.3
    // @description 在 V2EX 帖子详情页自动发现 Base64 字符串并在其后追加解码结果;若解码为 URL 则渲染为可点击链接(在新标签打开)。仅处理帖子正文与回复内容,避免页面卡死。
    // @author zhuziyi
    // @match https://v2ex.com/t/*
    // @match https://*.v2ex.com/t/*
    // @grant none
    // @run-at document-idle
    // @supportURL https://gist.github.com/zhuziyi1989/c728adf9c1d569e74362ca00818fe979
    // @homepageURL https://gist.github.com/zhuziyi1989/c728adf9c1d569e74362ca00818fe979
    // @downloadURL https://gist.github.com/zhuziyi1989/c728adf9c1d569e74362ca00818fe979/raw/81f269008d5d9e137a04746a9582c21d68a52a45/v2ex-base64-enhance.js
    // @updateURL https://gist.github.com/zhuziyi1989/c728adf9c1d569e74362ca00818fe979/raw/81f269008d5d9e137a04746a9582c21d68a52a45/v2ex-base64-enhance.js
    // ==/UserScript==

    (function () {
    "use strict";

    const MIN_B64_LEN = 16; // 最小 Base64 长度阈值,按需调整
    const SKIP_TAGS = new Set([
    "SCRIPT",
    "STYLE",
    "CODE",
    "PRE",
    "TEXTAREA",
    "INPUT",
    "A",
    ]);
    const B64_RE = new RegExp(
    `([A-Za-z0-9_\\-+\\/]{${MIN_B64_LEN},}={0,2})`,
    "g"
    );
    const APP_MARK = "data-b64-script"; // 我们插入节点时会打上这个标记

    // 记录已处理过的文本节点,避免重复处理
    const processedTextNodes = new WeakSet();

    function normalizeBase64(b64) {
    let s = b64.replace(/\s+/g, "");
    s = s.replace(/-/g, "+").replace(/_/g, "/");
    const pad = s.length % 4;
    if (pad === 1) return null;
    if (pad !== 0) s += "=".repeat(4 - pad);
    return s;
    }

    function decodeBase64ToString(b64) {
    try {
    const norm = normalizeBase64(b64);
    if (!norm) return null;
    const binary = atob(norm);
    // binary 是 Latin1,需要转为 UTF-8
    let percentEncoded = "";
    for (let i = 0; i < binary.length; i++) {
    const code = binary.charCodeAt(i);
    percentEncoded += "%" + ("00" + code.toString(16)).slice(-2);
    }
    try {
    return decodeURIComponent(percentEncoded);
    } catch (e) {
    // 退回为原 binary(可能是 Latin1 文本)
    return binary;
    }
    } catch (e) {
    return null;
    }
    }

    function looksReadable(s) {
    if (!s || s.length < 3) return false;
    if (/[\u4e00-\u9fff]/.test(s)) return true; // 含中文
    if (/(https?:\/\/|www\.)/i.test(s)) return true;
    // 至少有 60% 的可打印 ASCII 字符
    const printableCount = (s.match(/[\x20-\x7E]/g) || []).length;
    if (printableCount / s.length >= 0.6) return true;
    return false;
    }

    function createAppendNode(decodedText) {
    const wrapper = document.createElement("span");
    wrapper.setAttribute(APP_MARK, "1");
    wrapper.style.marginLeft = "6px";
    wrapper.style.fontSize = "0.95em";
    wrapper.style.verticalAlign = "baseline";

    const trimmed = decodedText.trim();
    const isURL = /^https?:\/\//i.test(trimmed);

    if (isURL) {
    // 全角括号包裹的可点链接
    const left = document.createTextNode("(");
    const right = document.createTextNode(")");

    const a = document.createElement("a");
    a.href = trimmed;
    a.target = "_blank";
    a.rel = "noopener noreferrer";
    a.textContent = trimmed;
    // 不强制设置颜色,保持与站点主题兼容;若需自定义可修改以下两行
    a.style.textDecoration = "underline";
    a.style.cursor = "pointer";

    wrapper.appendChild(left);
    wrapper.appendChild(a);
    wrapper.appendChild(right);
    } else {
    wrapper.style.color = "#888";
    wrapper.textContent = `(${trimmed})`;
    }

    return wrapper;
    }

    function processTextNode(textNode) {
    if (!textNode || !textNode.nodeValue) return;
    if (processedTextNodes.has(textNode)) return;

    const parent = textNode.parentNode;
    if (!parent || SKIP_TAGS.has(parent.tagName)) return;
    // 如果父元素或祖先已是我们插入内容的标记,则跳过(避免在我们的 span 内再次处理)
    if (parent.closest && parent.closest("[" + APP_MARK + "]")) return;

    const text = textNode.nodeValue;
    B64_RE.lastIndex = 0;
    let match;
    let lastIndex = 0;
    const frag = document.createDocumentFragment();
    let anyMatch = false;

    while ((match = B64_RE.exec(text)) !== null) {
    const b64 = match[1];
    // 把前面的纯文本加入
    const before = text.slice(lastIndex, match.index);
    if (before) frag.appendChild(document.createTextNode(before));

    // 原 Base64 文本(保持原样)
    const origNode = document.createTextNode(b64);
    frag.appendChild(origNode);

    // 尝试解码并判断是否可读
    const decoded = decodeBase64ToString(b64);
    if (decoded && looksReadable(decoded)) {
    const appendNode = createAppendNode(decoded);
    frag.appendChild(appendNode);
    }

    lastIndex = match.index + b64.length;
    anyMatch = true;
    }

    if (!anyMatch) {
    // 标记为已检查过(避免下次又检查同一节点)
    processedTextNodes.add(textNode);
    return;
    }

    // 尾部文本
    const tail = text.slice(lastIndex);
    if (tail) frag.appendChild(document.createTextNode(tail));

    // 替换原 text node(注意:替换后原节点不再存在)
    try {
    parent.replaceChild(frag, textNode);
    } catch (e) {
    // 如果替换失败(极少见),仍将该节点标记以避免重复
    processedTextNodes.add(textNode);
    return;
    }
    // 无需标记新创建的文本节点;但为了避免再次检查同一位置,我们可以标记父元素(轻量)
    // 在父元素上打一个短期属性,表明它已经被扫描(不会影响页面)
    try {
    parent.setAttribute("data-b64-scanned", Date.now().toString());
    } catch (e) {
    // 忽略不可设置属性的情况(例如某些 SVG 节点)
    }
    }

    function walkAndProcess(root) {
    if (!root) return;
    const walker = document.createTreeWalker(
    root,
    NodeFilter.SHOW_TEXT,
    {
    acceptNode(node) {
    if (!node.nodeValue || !node.nodeValue.trim())
    return NodeFilter.FILTER_REJECT;
    const p = node.parentNode;
    if (!p) return NodeFilter.FILTER_REJECT;
    if (SKIP_TAGS.has(p.tagName)) return NodeFilter.FILTER_REJECT;
    if (p.closest && p.closest("[" + APP_MARK + "]"))
    return NodeFilter.FILTER_REJECT;
    if (processedTextNodes.has(node)) return NodeFilter.FILTER_REJECT;
    return NodeFilter.FILTER_ACCEPT;
    },
    },
    false
    );

    const nodes = [];
    let n;
    while ((n = walker.nextNode())) nodes.push(n);
    for (const tn of nodes) processTextNode(tn);
    }

    // 找到需要处理的区域(帖子正文与回复)
    function findTargets() {
    const nodes = [];
    const main = document.querySelector(".topic_content");
    if (main) nodes.push(main);
    // 回复区常见类名
    document
    .querySelectorAll(".reply_content, .message, .topic .cell .reply_content")
    .forEach((n) => {
    if (n) nodes.push(n);
    });
    return nodes;
    }

    function initScan() {
    const targets = findTargets();
    if (targets.length === 0) return;
    for (const t of targets) walkAndProcess(t);
    }

    // 监听帖子区域的变化(只对目标区域设置 observer)
    const observers = [];
    function initObservers() {
    const targets = findTargets();
    if (targets.length === 0) return;

    for (const t of targets) {
    const mo = new MutationObserver((mutations) => {
    for (const m of mutations) {
    // 跳过我们自己插入的节点(根节点带标记)
    if (m.addedNodes && m.addedNodes.length) {
    for (const node of m.addedNodes) {
    if (node.nodeType === Node.ELEMENT_NODE) {
    // 如果新增节点是我们自己创建的标记子树,跳过
    if (node.hasAttribute && node.hasAttribute(APP_MARK)) continue;
    // 如果新增元素内部含有我们的标记,也跳过
    if (
    node.querySelector &&
    node.querySelector("[" + APP_MARK + "]")
    )
    continue;
    // 对新增元素内部做扫描
    walkAndProcess(node);
    } else if (node.nodeType === Node.TEXT_NODE) {
    // 直接处理文本节点
    processTextNode(node);
    }
    }
    }
    if (m.type === "characterData" && m.target) {
    processTextNode(m.target);
    }
    }
    });
    mo.observe(t, { childList: true, subtree: true, characterData: true });
    observers.push(mo);
    }
    }

    // 初次运行
    initScan();
    initObservers();

    // 暴露一个手动触发的函数(控制台可用),方便调试/手动重新扫描
    window.__v2ex_b64_rescan = function () {
    initScan();
    console.log("v2ex-base64: manual rescan done");
    };

    console.log("v2ex-base64 (post-only) loaded");

    // ============ 新增:Base64 加密与解密 选中文本 ============

    // Base64 编码
    function encodeBase64(str) {
    try {
    return btoa(unescape(encodeURIComponent(str)));
    } catch (e) {
    return str;
    }
    }

    // Base64 解码
    function decodeBase64(str) {
    try {
    return decodeURIComponent(escape(atob(str)));
    } catch (e) {
    return str;
    }
    }

    // ============ 新:自动隐藏按钮 ============

    function updateButtonVisibility() {
    const textarea = document.querySelector("#reply_content");
    const encodeBtn = document.getElementById("base64_encode_btn");
    const decodeBtn = document.getElementById("base64_decode_btn");
    if (!textarea || !encodeBtn || !decodeBtn) return;

    const start = textarea.selectionStart;
    const end = textarea.selectionEnd;

    // 无选区 → 按钮隐藏,但保留占位
    if (start === end) {
    encodeBtn.style.visibility = "hidden";
    decodeBtn.style.visibility = "hidden";
    } else {
    encodeBtn.style.visibility = "visible";
    decodeBtn.style.visibility = "visible";
    }
    }

    function addBase64Buttons() {
    const replyBtn = document.querySelector(
    'input[type="submit"][value="回复"]'
    );
    replyBtn.parentElement.style.justifyContent = "flex-start";
    replyBtn.parentElement.style.flexDirection = "row-reverse";
    const textarea = document.querySelector("#reply_content");

    if (!replyBtn || !textarea) return;
    if (document.getElementById("base64_encode_btn")) return;

    // 按钮创建器
    function createBtn(text, id) {
    const btn = document.createElement("button");
    btn.id = id;
    btn.textContent = text;
    btn.style.marginRight = "8px";
    btn.style.padding = "5px 8px";
    btn.style.border = "1px solid #f5f5f5";
    btn.style.background = "#f5f5f5";
    btn.style.borderRadius = "5px";
    btn.style.cursor = "pointer";
    btn.style.fontSize = "12px";
    btn.style.lineHeight = "1.4";
    btn.style.color = "#9349ff";
    btn.style.visibility = "hidden"; // 初始隐藏
    return btn;
    }

    const encodeBtn = createBtn("🔐 Base64编码", "base64_encode_btn");
    encodeBtn.addEventListener("click", (e) => {
    e.preventDefault();
    const start = textarea.selectionStart;
    const end = textarea.selectionEnd;
    const txt = textarea.value.substring(start, end);
    textarea.value =
    textarea.value.substring(0, start) +
    encodeBase64(txt) +
    textarea.value.substring(end);
    textarea.selectionStart = textarea.selectionEnd =
    start + encodeBase64(txt).length;
    textarea.focus();
    updateButtonVisibility();
    });

    const decodeBtn = createBtn("🔓 Base64解码", "base64_decode_btn");
    decodeBtn.addEventListener("click", (e) => {
    e.preventDefault();
    const start = textarea.selectionStart;
    const end = textarea.selectionEnd;
    const txt = textarea.value.substring(start, end);
    textarea.value =
    textarea.value.substring(0, start) +
    decodeBase64(txt) +
    textarea.value.substring(end);
    textarea.selectionStart = textarea.selectionEnd =
    start + decodeBase64(txt).length;
    textarea.focus();
    updateButtonVisibility();
    });

    replyBtn.parentNode.insertBefore(encodeBtn, replyBtn.nextSibling);
    replyBtn.parentNode.insertBefore(decodeBtn, encodeBtn.nextSibling);

    // 监听 selection 改变 → 控制可见性
    ["select", "keyup", "mouseup"].forEach((evt) => {
    textarea.addEventListener(evt, updateButtonVisibility);
    });
    }

    setTimeout(addBase64Buttons, 1500);
    })();
  5. zhuziyi1989 revised this gist Oct 17, 2025. 1 changed file with 35 additions and 0 deletions.
    35 changes: 35 additions & 0 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,35 @@
    # 评论区回复测试

    ---

    @wangdada #138 怎么方便的解密 Base64 啊?有没有插件推荐一个。

    试试我分享的油猴脚本[在 V2EX 帖子详情页自动发现 Base64 字符串并在其后追加解码结果](https://gist.github.com/zhuziyi1989/c728adf9c1d569e74362ca00818fe979),测试效果如下。

    ① 、未使用插件时显示

    <img width="781" height="413" alt="1" src="https://gist.github.com/user-attachments/assets/9795397a-c0ed-4f72-8903-ea3700d00327" />

    `[未使用插件时显示](https://i.imgur.com/Joi0n9p.png)`

    ② 、使用插件后显示为

    <img width="784" height="458" alt="2" src="https://gist.github.com/user-attachments/assets/3c9ef4b3-9284-49d3-af1e-740f38e78254" />

    `[使用插件后显示为](https://i.imgur.com/J5sqJNo.png)`

    ③ 、在评论区选中输入的文本可直接解密、加密替换

    ![4-min](https://gist.github.com/user-attachments/assets/fe037f81-9f30-4bb3-8375-2c6988db2b4c)

    `[在评论区选中输入的文本可直接解密、加密替换-GIF演示](https://i.imgur.com/qwgmHdN.gif)`

    以下为测试数据:

    国内打开 aHR0cHM6Ly8xcy5iaWdtZW9rLm1lL3VzZXIjL3JlZ2lzdGVyP2NvZGU9SW92V09wS0Y=

    科学上网 aHR0cHM6Ly93d3cuYmlnbWUucHJvL3VzZXIjL3JlZ2lzdGVyP2NvZGU9a2JYRjNrSmU=

    我是一个测试文本 5oiR5piv5LiA5Liq5rWL6K+V5paH5pys

    我是一个测试邮箱 cm9vdEBnbWFpbC5jb20=
  6. zhuziyi1989 renamed this gist Oct 17, 2025. 1 changed file with 0 additions and 0 deletions.
    File renamed without changes.
  7. zhuziyi1989 revised this gist Oct 17, 2025. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion V2EX Base64 Enhance.js
    Original file line number Diff line number Diff line change
    @@ -1,5 +1,5 @@
    // ==UserScript==
    // @name V2EX Base64 自动解析增强版
    // @name V2EX Base64 Enhance 自动解析增强版
    // @namespace http://tampermonkey.net/
    // @version 1.3
    // @description 在 V2EX 帖子详情页自动发现 Base64 字符串并在其后追加解码结果;若解码为 URL 则渲染为可点击链接(在新标签打开)。仅处理帖子正文与回复内容,避免页面卡死。
  8. zhuziyi1989 revised this gist Oct 17, 2025. 1 changed file with 389 additions and 1 deletion.
    390 changes: 389 additions & 1 deletion V2EX Base64 Enhance.js
    Original file line number Diff line number Diff line change
    @@ -1 +1,389 @@
    // ==UserScript==
    // @name V2EX Base64 自动解析增强版
    // @namespace http://tampermonkey.net/
    // @version 1.3
    // @description 在 V2EX 帖子详情页自动发现 Base64 字符串并在其后追加解码结果;若解码为 URL 则渲染为可点击链接(在新标签打开)。仅处理帖子正文与回复内容,避免页面卡死。
    // @author zhuziyi
    // @match https://v2ex.com/t/*
    // @match https://*.v2ex.com/t/*
    // @grant none
    // @run-at document-idle
    // @supportURL https://gist.github.com/zhuziyi1989/c728adf9c1d569e74362ca00818fe979
    // @homepageURL https://gist.github.com/zhuziyi1989/c728adf9c1d569e74362ca00818fe979
    // @downloadURL https://gist.github.com/zhuziyi1989/c728adf9c1d569e74362ca00818fe979/raw/97adee1329d4de9f6ef21eeb500006dea9998508/V2EX%2520Base64%2520Enhance.js
    // @updateURL https://gist.github.com/zhuziyi1989/c728adf9c1d569e74362ca00818fe979/raw/97adee1329d4de9f6ef21eeb500006dea9998508/V2EX%2520Base64%2520Enhance.js
    // ==/UserScript==

    (function () {
    "use strict";

    const MIN_B64_LEN = 16; // 最小 Base64 长度阈值,按需调整
    const SKIP_TAGS = new Set([
    "SCRIPT",
    "STYLE",
    "CODE",
    "PRE",
    "TEXTAREA",
    "INPUT",
    "A",
    ]);
    const B64_RE = new RegExp(
    `([A-Za-z0-9_\\-+\\/]{${MIN_B64_LEN},}={0,2})`,
    "g"
    );
    const APP_MARK = "data-b64-script"; // 我们插入节点时会打上这个标记

    // 记录已处理过的文本节点,避免重复处理
    const processedTextNodes = new WeakSet();

    function normalizeBase64(b64) {
    let s = b64.replace(/\s+/g, "");
    s = s.replace(/-/g, "+").replace(/_/g, "/");
    const pad = s.length % 4;
    if (pad === 1) return null;
    if (pad !== 0) s += "=".repeat(4 - pad);
    return s;
    }

    function decodeBase64ToString(b64) {
    try {
    const norm = normalizeBase64(b64);
    if (!norm) return null;
    const binary = atob(norm);
    // binary 是 Latin1,需要转为 UTF-8
    let percentEncoded = "";
    for (let i = 0; i < binary.length; i++) {
    const code = binary.charCodeAt(i);
    percentEncoded += "%" + ("00" + code.toString(16)).slice(-2);
    }
    try {
    return decodeURIComponent(percentEncoded);
    } catch (e) {
    // 退回为原 binary(可能是 Latin1 文本)
    return binary;
    }
    } catch (e) {
    return null;
    }
    }

    function looksReadable(s) {
    if (!s || s.length < 3) return false;
    if (/[\u4e00-\u9fff]/.test(s)) return true; // 含中文
    if (/(https?:\/\/|www\.)/i.test(s)) return true;
    // 至少有 60% 的可打印 ASCII 字符
    const printableCount = (s.match(/[\x20-\x7E]/g) || []).length;
    if (printableCount / s.length >= 0.6) return true;
    return false;
    }

    function createAppendNode(decodedText) {
    const wrapper = document.createElement("span");
    wrapper.setAttribute(APP_MARK, "1");
    wrapper.style.marginLeft = "6px";
    wrapper.style.fontSize = "0.95em";
    wrapper.style.verticalAlign = "baseline";

    const trimmed = decodedText.trim();
    const isURL = /^https?:\/\//i.test(trimmed);

    if (isURL) {
    // 全角括号包裹的可点链接
    const left = document.createTextNode("(");
    const right = document.createTextNode(")");

    const a = document.createElement("a");
    a.href = trimmed;
    a.target = "_blank";
    a.rel = "noopener noreferrer";
    a.textContent = trimmed;
    // 不强制设置颜色,保持与站点主题兼容;若需自定义可修改以下两行
    a.style.textDecoration = "underline";
    a.style.cursor = "pointer";

    wrapper.appendChild(left);
    wrapper.appendChild(a);
    wrapper.appendChild(right);
    } else {
    wrapper.style.color = "#888";
    wrapper.textContent = `(${trimmed})`;
    }

    return wrapper;
    }

    function processTextNode(textNode) {
    if (!textNode || !textNode.nodeValue) return;
    if (processedTextNodes.has(textNode)) return;

    const parent = textNode.parentNode;
    if (!parent || SKIP_TAGS.has(parent.tagName)) return;
    // 如果父元素或祖先已是我们插入内容的标记,则跳过(避免在我们的 span 内再次处理)
    if (parent.closest && parent.closest("[" + APP_MARK + "]")) return;

    const text = textNode.nodeValue;
    B64_RE.lastIndex = 0;
    let match;
    let lastIndex = 0;
    const frag = document.createDocumentFragment();
    let anyMatch = false;

    while ((match = B64_RE.exec(text)) !== null) {
    const b64 = match[1];
    // 把前面的纯文本加入
    const before = text.slice(lastIndex, match.index);
    if (before) frag.appendChild(document.createTextNode(before));

    // 原 Base64 文本(保持原样)
    const origNode = document.createTextNode(b64);
    frag.appendChild(origNode);

    // 尝试解码并判断是否可读
    const decoded = decodeBase64ToString(b64);
    if (decoded && looksReadable(decoded)) {
    const appendNode = createAppendNode(decoded);
    frag.appendChild(appendNode);
    }

    lastIndex = match.index + b64.length;
    anyMatch = true;
    }

    if (!anyMatch) {
    // 标记为已检查过(避免下次又检查同一节点)
    processedTextNodes.add(textNode);
    return;
    }

    // 尾部文本
    const tail = text.slice(lastIndex);
    if (tail) frag.appendChild(document.createTextNode(tail));

    // 替换原 text node(注意:替换后原节点不再存在)
    try {
    parent.replaceChild(frag, textNode);
    } catch (e) {
    // 如果替换失败(极少见),仍将该节点标记以避免重复
    processedTextNodes.add(textNode);
    return;
    }
    // 无需标记新创建的文本节点;但为了避免再次检查同一位置,我们可以标记父元素(轻量)
    // 在父元素上打一个短期属性,表明它已经被扫描(不会影响页面)
    try {
    parent.setAttribute("data-b64-scanned", Date.now().toString());
    } catch (e) {
    // 忽略不可设置属性的情况(例如某些 SVG 节点)
    }
    }

    function walkAndProcess(root) {
    if (!root) return;
    const walker = document.createTreeWalker(
    root,
    NodeFilter.SHOW_TEXT,
    {
    acceptNode(node) {
    if (!node.nodeValue || !node.nodeValue.trim())
    return NodeFilter.FILTER_REJECT;
    const p = node.parentNode;
    if (!p) return NodeFilter.FILTER_REJECT;
    if (SKIP_TAGS.has(p.tagName)) return NodeFilter.FILTER_REJECT;
    if (p.closest && p.closest("[" + APP_MARK + "]"))
    return NodeFilter.FILTER_REJECT;
    if (processedTextNodes.has(node)) return NodeFilter.FILTER_REJECT;
    return NodeFilter.FILTER_ACCEPT;
    },
    },
    false
    );

    const nodes = [];
    let n;
    while ((n = walker.nextNode())) nodes.push(n);
    for (const tn of nodes) processTextNode(tn);
    }

    // 找到需要处理的区域(帖子正文与回复)
    function findTargets() {
    const nodes = [];
    const main = document.querySelector(".topic_content");
    if (main) nodes.push(main);
    // 回复区常见类名
    document
    .querySelectorAll(".reply_content, .message, .topic .cell .reply_content")
    .forEach((n) => {
    if (n) nodes.push(n);
    });
    return nodes;
    }

    function initScan() {
    const targets = findTargets();
    if (targets.length === 0) return;
    for (const t of targets) walkAndProcess(t);
    }

    // 监听帖子区域的变化(只对目标区域设置 observer)
    const observers = [];
    function initObservers() {
    const targets = findTargets();
    if (targets.length === 0) return;

    for (const t of targets) {
    const mo = new MutationObserver((mutations) => {
    for (const m of mutations) {
    // 跳过我们自己插入的节点(根节点带标记)
    if (m.addedNodes && m.addedNodes.length) {
    for (const node of m.addedNodes) {
    if (node.nodeType === Node.ELEMENT_NODE) {
    // 如果新增节点是我们自己创建的标记子树,跳过
    if (node.hasAttribute && node.hasAttribute(APP_MARK)) continue;
    // 如果新增元素内部含有我们的标记,也跳过
    if (
    node.querySelector &&
    node.querySelector("[" + APP_MARK + "]")
    )
    continue;
    // 对新增元素内部做扫描
    walkAndProcess(node);
    } else if (node.nodeType === Node.TEXT_NODE) {
    // 直接处理文本节点
    processTextNode(node);
    }
    }
    }
    if (m.type === "characterData" && m.target) {
    processTextNode(m.target);
    }
    }
    });
    mo.observe(t, { childList: true, subtree: true, characterData: true });
    observers.push(mo);
    }
    }

    // 初次运行
    initScan();
    initObservers();

    // 暴露一个手动触发的函数(控制台可用),方便调试/手动重新扫描
    window.__v2ex_b64_rescan = function () {
    initScan();
    console.log("v2ex-base64: manual rescan done");
    };

    console.log("v2ex-base64 (post-only) loaded");

    // ============ 新增:Base64 加密与解密 选中文本 ============

    // Base64 编码
    function encodeBase64(str) {
    try {
    return btoa(unescape(encodeURIComponent(str)));
    } catch (e) {
    return str;
    }
    }

    // Base64 解码
    function decodeBase64(str) {
    try {
    return decodeURIComponent(escape(atob(str)));
    } catch (e) {
    return str;
    }
    }

    // ============ 新:自动隐藏按钮 ============

    function updateButtonVisibility() {
    const textarea = document.querySelector("#reply_content");
    const encodeBtn = document.getElementById("base64_encode_btn");
    const decodeBtn = document.getElementById("base64_decode_btn");
    if (!textarea || !encodeBtn || !decodeBtn) return;

    const start = textarea.selectionStart;
    const end = textarea.selectionEnd;

    // 无选区 → 按钮隐藏,但保留占位
    if (start === end) {
    encodeBtn.style.visibility = "hidden";
    decodeBtn.style.visibility = "hidden";
    } else {
    encodeBtn.style.visibility = "visible";
    decodeBtn.style.visibility = "visible";
    }
    }

    function addBase64Buttons() {
    const replyBtn = document.querySelector(
    'input[type="submit"][value="回复"]'
    );
    replyBtn.parentElement.style.justifyContent = "flex-start";
    replyBtn.parentElement.style.flexDirection = "row-reverse";
    const textarea = document.querySelector("#reply_content");

    if (!replyBtn || !textarea) return;
    if (document.getElementById("base64_encode_btn")) return;

    // 按钮创建器
    function createBtn(text, id) {
    const btn = document.createElement("button");
    btn.id = id;
    btn.textContent = text;
    btn.style.marginRight = "8px";
    btn.style.padding = "5px 8px";
    btn.style.border = "1px solid #f5f5f5";
    btn.style.background = "#f5f5f5";
    btn.style.borderRadius = "5px";
    btn.style.cursor = "pointer";
    btn.style.fontSize = "12px";
    btn.style.lineHeight = "1.4";
    btn.style.color = "#9349ff";
    btn.style.visibility = "hidden"; // 初始隐藏
    return btn;
    }

    const encodeBtn = createBtn("🔐 Base64编码", "base64_encode_btn");
    encodeBtn.addEventListener("click", (e) => {
    e.preventDefault();
    const start = textarea.selectionStart;
    const end = textarea.selectionEnd;
    const txt = textarea.value.substring(start, end);
    textarea.value =
    textarea.value.substring(0, start) +
    encodeBase64(txt) +
    textarea.value.substring(end);
    textarea.selectionStart = textarea.selectionEnd =
    start + encodeBase64(txt).length;
    textarea.focus();
    updateButtonVisibility();
    });

    const decodeBtn = createBtn("🔓 Base64解码", "base64_decode_btn");
    decodeBtn.addEventListener("click", (e) => {
    e.preventDefault();
    const start = textarea.selectionStart;
    const end = textarea.selectionEnd;
    const txt = textarea.value.substring(start, end);
    textarea.value =
    textarea.value.substring(0, start) +
    decodeBase64(txt) +
    textarea.value.substring(end);
    textarea.selectionStart = textarea.selectionEnd =
    start + decodeBase64(txt).length;
    textarea.focus();
    updateButtonVisibility();
    });

    replyBtn.parentNode.insertBefore(encodeBtn, replyBtn.nextSibling);
    replyBtn.parentNode.insertBefore(decodeBtn, encodeBtn.nextSibling);

    // 监听 selection 改变 → 控制可见性
    ["select", "keyup", "mouseup"].forEach((evt) => {
    textarea.addEventListener(evt, updateButtonVisibility);
    });
    }

    setTimeout(addBase64Buttons, 1500);
    })();
  9. zhuziyi1989 created this gist Oct 17, 2025.
    1 change: 1 addition & 0 deletions V2EX Base64 Enhance.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1 @@